UNPKG

30.4 kBMarkdownView Raw
1<h1 align="center">
2 <a href="https://elbywan.github.io/wretch"><img src="https://cdn.rawgit.com/elbywan/wretch/08831345/wretch.svg" alt="wretch-logo" width="220px"></a><br>
3 <br>
4 <a href="https://elbywan.github.io/wretch">Wretch</a><br>
5 <br>
6 <a href="https://elbywan.github.io/wretch"><img alt="homepage-badge" src="https://img.shields.io/website-up-down-green-red/http/shields.io.svg?label=wretch-homepage"></a>
7 <a href="https://travis-ci.org/elbywan/wretch"><img alt="travis-badge" src="https://travis-ci.org/elbywan/wretch.svg?branch=master"></a>
8 <a href="https://www.npmjs.com/package/wretch"><img alt="npm-badge" src="https://img.shields.io/npm/v/wretch.svg?colorB=ff733e" height="20"></a>
9 <a href="https://www.npmjs.com/package/wretch"><img alt="npm-downloads-badge" src="https://img.shields.io/npm/dm/wretch.svg?colorB=53aabb" height="20"></a>
10 <a href="https://coveralls.io/github/elbywan/wretch?branch=master"><img src="https://coveralls.io/repos/github/elbywan/wretch/badge.svg?branch=master" alt="Coverage Status"></a>
11 <a href="https://github.com/elbywan/wretch/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license-badge" height="20"></a>
12</h1>
13<h4 align="center">
14 A tiny (&lt; 2.5Kb g-zipped) wrapper built around fetch with an intuitive syntax.
15</h4>
16<h5 align="center">
17 <i>f[ETCH] [WR]apper</i>
18</h6>
19
20<br>
21
22##### Wretch 1.2 is now live 🎉 ! Please check out the [changelog](https://github.com/elbywan/wretch/blob/master/CHANGELOG.md) after each update for new features and breaking changes. If you want to try out the hot stuff, look at the [dev](https://github.com/elbywan/wretch/tree/dev) branch.
23
24##### A collection of middlewares is available through the [wretch-middlewares](https://github.com/elbywan/wretch-middlewares) package! 📦
25
26# Table of Contents
27
28* [Motivation](#motivation)
29* [Installation](#installation)
30* [Compatibility](#compatibility)
31* [Usage](#usage)
32* [Api](#api)
33* [License](#license)
34
35# Motivation
36
37#### Because having to write a second callback to process a response body feels awkward.
38
39```javascript
40// Fetch needs a second callback to process the response body
41
42fetch("examples/example.json")
43 .then(response => response.json())
44 .then(json => {
45 //Do stuff with the parsed json
46 })
47```
48
49```javascript
50// Wretch does it for you
51
52// Use .res for the raw response, .text for raw text, .json for json, .blob for a blob ...
53wretch("examples/example.json")
54 .get()
55 .json(json => {
56 // Do stuff with the parsed json
57 })
58```
59
60#### Because manually checking and throwing every request error code is fastidious.
61
62```javascript
63// Fetch won’t reject on HTTP error status
64
65fetch("anything")
66 .then(response => {
67 if(!response.ok) {
68 if(response.status === 404) throw new Error("Not found")
69 else if(response.status === 401) throw new Error("Unauthorized")
70 else if(response.status === 418) throw new Error("I'm a teapot !")
71 else throw new Error("Other error")
72 }
73 else // ...
74 })
75 .then(data => /* ... */)
76 .catch(error => { /* ... */ })
77```
78
79```javascript
80// Wretch throws when the response is not successful and contains helper methods to handle common codes
81
82wretch("anything")
83 .get()
84 .notFound(error => { /* ... */ })
85 .unauthorized(error => { /* ... */ })
86 .error(418, error => { /* ... */ })
87 .res(response => /* ... */)
88 .catch(error => { /* uncaught errors */ })
89```
90
91#### Because sending a json object should be easy.
92
93```javascript
94// With fetch you have to set the header, the method and the body manually
95
96fetch("endpoint", {
97 method: "POST",
98 headers: { "Content-Type": "application/json" },
99 body: JSON.stringify({ "hello": "world" })
100}).then(response => /* ... */)
101// Omitting the data retrieval and error management parts
102```
103
104```javascript
105// With wretch, you have shorthands at your disposal
106
107wretch("endpoint")
108 .post({ "hello": "world" })
109 .res(response => /* ... */)
110```
111
112#### Because configuration should not rhyme with repetition.
113
114```javascript
115// Wretch object is immutable which means that you can configure, store and reuse instances
116
117// Cross origin authenticated requests on an external API
118const externalApi = wretch()
119 // Set the base url
120 .url("http://external.api")
121 // Authorization header
122 .auth(`Bearer ${ token }`)
123 // Cors fetch options
124 .options({ credentials: "include", mode: "cors" })
125 // Handle 403 errors
126 .resolve(_ => _.forbidden(handle403))
127
128// Fetch a resource
129externalApi
130 .url("/resource/1")
131 // Add a custom header for this request
132 .headers({ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT" })
133 .get()
134 .json(handleResource)
135// Post a resource
136externalApi
137 .url("/resource")
138 .post({ "Shiny new": "resource object" })
139 .json(handleNewResourceResult)
140```
141
142# Installation
143
144## Npm
145
146```sh
147npm i wretch
148```
149
150## Clone
151
152```sh
153git clone https://github.com/elbywan/wretch
154cd wretch
155npm install
156npm start
157```
158
159# Compatibility
160
161## Browsers
162
163Wretch is compatible with modern browsers out of the box.
164
165For older environments without fetch support, you should get a [polyfill](https://github.com/github/fetch).
166
167## Node.js
168
169Works with any [FormData](https://github.com/form-data/form-data) or [fetch](https://www.npmjs.com/package/node-fetch) polyfills.
170
171```javascript
172// The global way :
173
174global.fetch = require("node-fetch")
175global.FormData = require("form-data")
176global.URLSearchParams = require("url").URLSearchParams
177
178// Or the non-global way :
179
180wretch().polyfills({
181 fetch: require("node-fetch"),
182 FormData: require("form-data"),
183 URLSearchParams: require("url").URLSearchParams
184})
185```
186
187# Usage
188
189**Wretch is bundled using the UMD format (@`dist/bundle/wretch.js`) alongside es2015 modules (@`dist/index.js`) and typescript definitions.**
190
191## Import
192
193```html
194<!--- "wretch" will be attached to the global window object. -->
195<script src="https://unpkg.com/wretch"></script>
196```
197
198```typescript
199// es2015 modules
200import wretch from "wretch"
201
202// commonjs
203var wretch = require("wretch")
204```
205
206## Code
207
208**Wretcher objects are immutable.**
209
210```javascript
211wretch(url, options)
212
213 /* The "request" chain. */
214
215 .[helper method(s)]()
216 // [ Optional ]
217 // A set of helper methods to set the default options, set accept header, change the current url ...
218 .[body type]()
219 // [ Optional ]
220 // Serialize an object to json or FormData formats and sets the body & header field if needed
221 .[http method]()
222 // [ Required, ends the request chain ]
223 // Performs the get/put/post/delete/patch request
224
225 /* Fetch is called at this time. */
226 /* The request is sent, and from this point on you can chain catchers and call a response type handler. */
227
228 /* The "response" chain. */
229
230 .[catcher(s)]()
231 // [ Optional ]
232 // You can chain error handlers here
233 .[response type]()
234 // [ Required, ends the response chain ]
235 // Specify the data type you need, which will be parsed and handed to you
236
237 /* From this point wretch returns a standard Promise, so you can continue chaining actions afterwards. */
238
239 .then(/* ... */)
240 .catch(/* ... */)
241```
242
243# API
244
245* [Helper Methods](#helper-methods)
246* [Body Types](#body-types)
247* [Http Methods](#http-methods)
248* [Catchers](#catchers)
249* [Response Types](#response-types)
250* [Extras](#extras)
251
252------
253
254#### wretch(url = "", opts = {})
255
256Creates a new Wretcher object with an url and [vanilla fetch options](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch).
257
258## Helper Methods
259
260*Helper methods are optional and can be chained.*
261
262| [url](#urlurl-string-replace-boolean--false) | [query](#queryqp-object--string-replace-boolean) | [options](#optionsoptions-object-mixin-boolean--true) | [headers](#headersheadervalues-object) | [accept](#acceptheadervalue-string) | [content](#contentheadervalue-string) | [auth](#authheadervalue-string) | [catcher](#catchererrorid-number--string-catcher-error-wretchererror-originalrequest-wretcher--void) | [resolve](#resolvedoresolve-chain-responsechain-originalrequest-wretcher--responsechain--promise-clear--false) | [defaults](#defaultsopts-object-mixin-boolean--false) | [errorType](#errortypemethod-text--json--text) | [polyfills](#polyfillspolyfills-object) |
263|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
264
265#### url(url: string, replace: boolean = false)
266
267Appends or replaces the url.
268
269```js
270wretch().url("...").get().json(/* ... */)
271
272// Can be used to set a base url
273
274// Subsequent requests made using the 'blogs' object will be prefixed with "http://mywebsite.org/api/blogs"
275const blogs = wretch("http://mywebsite.org/api/blogs")
276
277// Perfect for CRUD apis
278const id = await blogs.post({ name: "my blog" }).json(_ => _.id)
279const blog = await blogs.url(`/${id}`).get().json()
280console.log(blog.name)
281
282await blogs.url(`/${id}`).delete().res()
283
284// And to replace the base url if needed :
285const noMoreBlogs = blogs.url("http://mywebsite.org/", true)
286```
287
288#### query(qp: object | string, replace: boolean)
289
290Converts a javascript object to query parameters, then appends this query string to the current url.
291String values are used as the query string verbatim.
292
293Pass `true` as the second argument to replace existing query parameters.
294
295```js
296let w = wretch("http://example.com")
297// url is http://example.com
298w = w.query({ a: 1, b: 2 })
299// url is now http://example.com?a=1&b=2
300w = w.query({ c: 3, d: [4, 5] })
301// url is now http://example.com?a=1&b=2c=3&d=4&d=5
302w = w.query("five&six&seven=eight")
303// url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eight
304w = w.query({ reset: true }, true)
305// url is now http://example.com?reset=true
306```
307
308#### options(options: Object, mixin: boolean = true)
309
310Sets the fetch options.
311
312```js
313wretch("...").options({ credentials: "same-origin" })
314```
315
316Wretch being immutable, you can store the object for later use.
317
318```js
319const corsWretch = wretch().options({ credentials: "include", mode: "cors" })
320
321corsWretch.url("http://endpoint1").get()
322corsWretch.url("http://endpoint2").get()
323```
324
325You can override instead of mixing in the existing options by passing a boolean flag.
326
327```js
328// By default options mixed in :
329
330wretch()
331 .options({ headers: { "Accept": "application/json" }})
332 .options({ encoding: "same-origin", headers: { "X-Custom": "Header" }})
333
334/*
335{
336 headers: { "Accept": "application/json", "X-Custom": "Header" },
337 encoding: "same-origin"
338}
339*/
340
341// With the flag, options are overridden :
342
343wretch()
344 .options({ headers: { "Accept": "application/json" }})
345 .options({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, false)
346
347/*
348{
349 headers: { "X-Custom": "Header" },
350 encoding: "same-origin"
351}
352*/
353```
354
355#### headers(headerValues: Object)
356
357Sets the request headers.
358
359```js
360wretch("...")
361 .headers({ "Content-Type": "text/plain", Accept: "application/json" })
362 .post("my text")
363 .json()
364```
365
366#### accept(headerValue: string)
367
368Shortcut to set the "Accept" header.
369
370```js
371wretch("...").accept("application/json")
372```
373
374#### content(headerValue: string)
375
376Shortcut to set the "Content-Type" header.
377
378```js
379wretch("...").content("application/json")
380```
381
382#### auth(headerValue: string)
383
384Shortcut to set the "Authorization" header.
385
386```js
387wretch("...").auth("Basic d3JldGNoOnJvY2tz")
388```
389
390#### catcher(errorId: number | string, catcher: (error: WretcherError, originalRequest: Wretcher) => void)
391
392Adds a [catcher](https://github.com/elbywan/wretch#catchers) which will be called on every subsequent request error.
393
394Very useful when you need to perform a repetitive action on a specific error code.
395
396```js
397const w = wretch()
398 .catcher(404, err => redirect("/routes/notfound", err.message))
399 .catcher(500, err => flashMessage("internal.server.error"))
400 .error("SyntaxError", err => log("bad.json"))
401
402// No need to catch 404 or 500 code or the json parsing error, they are already taken care of.
403w.url("http://myapi.com/get/something").get().json(json => /* ... */)
404
405// Default catchers can be overridden if needed.
406w.url("...").notFound(err => /* overrides the default 'redirect' catcher */)
407```
408
409The original request is passed along the error and can be used in order to perform an additional request.
410
411```js
412const reAuthOn401 = wretch()
413 .catcher(401, async (error, request) => {
414 // Renew credentials
415 const token = await wretch("/renewtoken").get().text()
416 storeToken(token)
417 // Replay the original request with new credentials
418 return request.auth(token).get().unauthorized(err => { throw err }).json()
419 })
420
421reAuthOn401.url("/resource")
422 .get()
423 .json() // <- Will only be called for the original promise
424 .then(callback) // <- Will be called for the original OR the replayed promise result
425```
426
427#### defer(callback: (originalRequest: Wretcher, url: string, options: Object) => Wretcher, clear = false)
428
429Defer wretcher methods that will be chained and called just before the request is performed.
430
431```js
432/* Small fictional example: deferred authentication */
433
434// If you cannot retrieve the auth token while configuring the wretch object you can use .defer to postpone the call
435const api = wretch("...").defer((w, url, options) => {
436 // If we are hitting the route /user…
437 if(/\/user/.test(url)) {
438 const { token } = options.context
439 return w.auth(token)
440 }
441 return w
442})
443
444// ... //
445
446const token = await getToken(request.session.user)
447
448// .auth gets called here automatically
449api.options({
450 context: { token }
451}).get().res()
452```
453
454#### resolve(doResolve: (chain: ResponseChain, originalRequest: Wretcher) => ResponseChain | Promise<any>, clear = false)
455
456Programs a resolver which will automatically be injected to perform response chain tasks.
457
458Very useful when you need to perform repetitive actions on the wretch response.
459
460*The clear argument, if set to true, removes previously defined resolvers.*
461
462```js
463// Program "response" chain actions early on
464const w = wretch()
465 .resolve(resolver => resolver
466 .perfs(_ => /* monitor every request */)
467 .json(_ => _ /* automatically parse and return json */))
468
469const myJson = await w.url("http://a.com").get()
470// Equivalent to wretch()
471// .url("http://a.com")
472// .get()
473// <- the resolver chain is automatically injected here !
474// .perfs(_ => /* ... */)
475// .json(_ => _)
476```
477
478#### defaults(opts: Object, mixin: boolean = false)
479
480Sets default fetch options which will be used for every subsequent requests.
481
482```js
483// Interestingly enough, default options are mixed in :
484
485wretch().defaults({ headers: { "Accept": "application/json" }})
486
487// The fetch request is sent with both headers.
488wretch("...", { headers: { "X-Custom": "Header" }}).get()
489```
490
491```js
492// You can mix in with the existing options instead of overriding them by passing a boolean flag :
493
494wretch().defaults({ headers: { "Accept": "application/json" }})
495wretch().defaults({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, true)
496
497/* The new options are :
498{
499 headers: { "Accept": "application/json", "X-Custom": "Header" },
500 encoding: "same-origin"
501}
502*/
503```
504
505#### errorType(method: "text" | "json" = "text")
506
507Sets the method (text, json ...) used to parse the data contained in the response body in case of an HTTP error.
508
509Persists for every subsequent requests.
510
511```js
512wretch().errorType("json")
513
514wretch("http://server/which/returns/an/error/with/a/json/body")
515 .get()
516 .res()
517 .catch(error => {
518 // error[errorType] (here, json) contains the parsed body
519 console.log(error.json))
520 }
521```
522
523#### polyfills(polyfills: Object)
524
525Sets the non-global polyfills which will be used for every subsequent calls.
526
527```javascript
528const fetch = require("node-fetch")
529const FormData = require("form-data")
530
531wretch().polyfills({
532 fetch: fetch,
533 FormData: FormData,
534 URLSearchParams: require("url").URLSearchParams
535})
536```
537
538## Body Types
539
540*A body type is only needed when performing put/patch/post requests with a body.*
541
542| [body](#bodycontents-any) | [json](#jsonjsobject-object) | [formData](#formdataformobject-object) | [formUrl](#formurlinput--object--string) |
543|-----|-----|-----|-----|
544
545#### body(contents: any)
546
547Sets the request body with any content.
548
549```js
550wretch("...").body("hello").put()
551// Note that calling an 'http verb' method with the body as an argument is equivalent:
552wretch("...").put("hello")
553```
554
555#### json(jsObject: Object)
556
557Sets the content type header, stringifies an object and sets the request body.
558
559```js
560const jsonObject = { a: 1, b: 2, c: 3 }
561wretch("...").json(jsonObject).post()
562// Note that calling an 'http verb' method with the object body as an argument is equivalent:
563wretch("...").post(jsonObject)
564
565```
566
567#### formData(formObject: Object)
568
569Converts the javascript object to a FormData and sets the request body.
570
571```js
572const form = {
573 hello: "world",
574 duck: "Muscovy"
575}
576wretch("...").formData(form).post()
577```
578
579#### formUrl(input: Object | string)
580
581Converts the input parameter to an url encoded string and sets the content-type header and body.
582If the input argument is already a string, skips the conversion part.
583
584```js
585const form = { a: 1, b: { c: 2 }}
586const alreadyEncodedForm = "a=1&b=%7B%22c%22%3A2%7D"
587
588// Automatically sets the content-type header to "application/x-www-form-urlencoded"
589wretch("...").formUrl(form).post()
590wretch("...").formUrl(alreadyEncodedForm).post()
591```
592
593## Http Methods
594
595**Required**
596
597*You can pass optional fetch options and body arguments to these methods as a shorthand.*
598
599```js
600// This shorthand:
601wretch().post({ json: 'body' }, { credentials: "same-origin" })
602// Is equivalent to:
603wretch().json({ json: 'body'}).options({ credentials: "same-origin" }).post()
604```
605
606| [get](#getopts) | [delete](#deleteopts) | [put](#putbody-opts) | [patch](#patchbody-opts) | [post](#postbody-opts) | [head](#headopts) | [opts](#optsopts) |
607|-----|-----|-----|-----|-----|-----|-----|
608
609#### get(options)
610
611Performs a get request.
612
613```js
614wretch("...").get()
615```
616
617#### delete(options)
618
619Performs a delete request.
620
621```js
622wretch("...").delete()
623```
624
625#### put(body, options)
626
627Performs a put request.
628
629```js
630wretch("...").json({...}).put()
631```
632
633#### patch(body, options)
634
635Performs a patch request.
636
637```js
638wretch("...").json({...}).patch()
639```
640
641#### post(body, options)
642
643Performs a post request.
644
645```js
646wretch("...").json({...}).post()
647```
648
649#### head(options)
650
651Performs a head request.
652
653```js
654wretch("...").head()
655```
656#### opts(options)
657
658Performs an options request.
659
660```js
661wretch("...").opts()
662```
663
664## Catchers
665
666*Catchers are optional, but if you do not provide them an error will still be thrown in case of an http error code received.*
667
668*Catchers can be chained.*
669
670| [badRequest](#badrequestcb-error-wretchererror-originalrequest-wretcher--any) | [unauthorized](#unauthorizedcb-error-wretchererror-originalrequest-wretcher--any) | [forbidden](#forbiddencb-error-wretchererror-originalrequest-wretcher--any) | [notFound](#notfoundcb-error-wretchererror-originalrequest-wretcher--any) | [timeout](#timeoutcb-error-wretchererror-originalrequest-wretcher--any) | [internalError](#internalerrorcb-error-wretchererror-originalrequest-wretcher--any) | [error](#errorerrorid-number--string-cb-error-wretchererror-originalrequest-wretcher--any) |
671|-----|-----|-----|-----|-----|-----|-----|
672
673```ts
674type WretcherError = Error & { status: number, response: WretcherResponse, text?: string, json?: Object }
675```
676
677```js
678wretch("...")
679 .get()
680 .badRequest(err => console.log(err.status))
681 .unauthorized(err => console.log(err.status))
682 .forbidden(err => console.log(err.status))
683 .notFound(err => console.log(err.status))
684 .timeout(err => console.log(err.status))
685 .internalError(err => console.log(err.status))
686 .error(418, err => console.log(err.status))
687 .res()
688```
689
690#### badRequest(cb: (error: WretcherError, originalRequest: Wretcher) => any)
691
692Syntactic sugar for `error(400, cb)`.
693
694#### unauthorized(cb: (error: WretcherError, originalRequest: Wretcher) => any)
695
696Syntactic sugar for `error(401, cb)`.
697
698#### forbidden(cb: (error: WretcherError, originalRequest: Wretcher) => any)
699
700Syntactic sugar for `error(403, cb)`.
701
702#### notFound(cb: (error: WretcherError, originalRequest: Wretcher) => any)
703
704Syntactic sugar for `error(404, cb)`.
705
706#### timeout(cb: (error: WretcherError, originalRequest: Wretcher) => any)
707
708Syntactic sugar for `error(418, cb)`.
709
710#### internalError(cb: (error: WretcherError, originalRequest: Wretcher) => any)
711
712Syntactic sugar for `error(500, cb)`.
713
714#### error(errorId: number | string, cb: (error: WretcherError, originalRequest: Wretcher) => any)
715
716Catches a specific error given its code or name and perform the callback.
717
718---------
719
720The original request is passed along the error and can be used in order to perform an additional request.
721
722```js
723wretch("/resource")
724 .get()
725 .unauthorized(async (error, req) => {
726 // Renew credentials
727 const token = await wretch("/renewtoken").get().text()
728 storeToken(token)
729 // Replay the original request with new credentials
730 return req.auth(token).get().unauthorized(err => { throw err }).json()
731 })
732 .json()
733 // The promise chain is preserved as expected
734 // ".then" will be performed on the result of the original request
735 // or the replayed one (if a 401 error was thrown)
736 .then(callback)
737```
738
739## Response Types
740
741**Required**
742
743*If an error is caught by catchers, the response type handler will not be called.*
744
745| [res](#rescb-response--response--any) | [json](#jsoncb-json--object--any) | [blob](#blobcb-blob--blob--any) | [formData](#formdatacb-fd--formdata--any) | [arrayBuffer](#arraybuffercb-ab--arraybuffer--any) | [text](#textcb-text--string--any) |
746|-----|-----|-----|-----|-----|-----|
747
748#### res(cb: (response : Response) => any)
749
750Raw Response handler.
751
752```js
753wretch("...").get().res(response => console.log(response.url))
754```
755
756#### json(cb: (json : Object) => any)
757
758Json handler.
759
760```js
761wretch("...").get().json(json => console.log(Object.keys(json)))
762```
763
764#### blob(cb: (blob : Blob) => any)
765
766Blob handler.
767
768```js
769wretch("...").get().blob(blob => /* ... */)
770```
771
772#### formData(cb: (fd : FormData) => any)
773
774FormData handler.
775
776```js
777wretch("...").get().formData(formData => /* ... */)
778```
779
780#### arrayBuffer(cb: (ab : ArrayBuffer) => any)
781
782ArrayBuffer handler.
783
784```js
785wretch("...").get().arrayBuffer(arrayBuffer => /* ... */)
786```
787
788#### text(cb: (text : string) => any)
789
790Text handler.
791
792```js
793wretch("...").get().text(txt => console.log(txt))
794```
795
796## Extras
797
798*A set of extra features.*
799
800| [Abortable requests](#abortable-requests) | [Performance API](#performance-api) | [Middlewares](#middlewares) |
801|-----|-----|-----|
802
803### Abortable requests
804
805*Only compatible with browsers that support [AbortControllers](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). Otherwise, you could use a (partial) [polyfill](https://www.npmjs.com/package/abortcontroller-polyfill).*
806
807Use case :
808
809```js
810const [c, w] = wretch("...")
811 .get()
812 .onAbort(_ => console.log("Aborted !"))
813 .controller()
814
815w.text(_ => console.log("should never be called"))
816c.abort()
817
818// Or :
819
820const controller = new AbortController()
821
822wretch("...")
823 .signal(controller)
824 .get()
825 .onAbort(_ => console.log("Aborted !"))
826 .text(_ => console.log("should never be called"))
827
828c.abort()
829```
830
831### signal(controller: AbortController)
832
833*Used at "request time", like an helper.*
834
835Associates a custom controller with the request.
836Useful when you need to use your own AbortController, otherwise wretch will create a new controller itself.
837
838```js
839const controller = new AbortController()
840
841// Associates the same controller with multiple requests
842
843wretch("url1")
844 .signal(controller)
845 .get()
846 .json(_ => /* ... */)
847wretch("url2")
848 .signal(controller)
849 .get()
850 .json(_ => /* ... */)
851
852// Aborts both requests
853
854controller.abort()
855```
856
857#### setTimeout(time: number, controller?: AbortController)
858
859*Used at "response time".*
860
861Aborts the request after a fixed time. If you use a custom AbortController associated with the request, pass it as the second argument.
862
863```js
864// 1 second timeout
865wretch("...").get().setTimeout(1000).json(_ => /* will not be called in case of a timeout */)
866```
867
868#### controller()
869
870*Used at "response time".*
871
872Returns the automatically generated AbortController alongside the current wretch response as a pair.
873
874```js
875// We need the controller outside the chain
876const [c, w] = wretch("url")
877 .get()
878 .controller()
879
880// Resume with the chain
881w.onAbort(_ => console.log("ouch")).json(_ => /* ... */)
882
883/* Later on ... */
884c.abort()
885```
886
887#### onAbort(cb: (error: AbortError) => any)
888
889*Used at "response time" like a catcher.*
890
891Catches an AbortError and performs the callback.
892
893### Performance API
894
895#### perfs(cb: (timings: PerformanceTiming) => void)
896
897Takes advantage of the Performance API ([browsers](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API) & [node.js](https://nodejs.org/api/perf_hooks.html)) to expose timings related to the underlying request.
898
899Browser timings are very accurate, node.js only contains raw measures.
900
901```js
902// Use perfs() before the response types (text, json, ...)
903wretch("...")
904 .get()
905 .perfs(timings => {
906 /* Will be called when the timings are ready. */
907 console.log(timings.startTime)
908 })
909 .res()
910 /* ... */
911```
912
913For node.js, there is a little extra work to do :
914
915```js
916// Node.js 8.5+ only
917const { performance, PerformanceObserver } = require("perf_hooks")
918
919wretch().polyfills({
920 fetch: function(url, opts) {
921 performance.mark(url + " - begin")
922 return fetch(url, opts).then(_ => {
923 performance.mark(url + " - end")
924 performance.measure(_.url, url + " - begin", url + " - end")
925 })
926 },
927 /* other polyfills ... */
928 performance: performance,
929 PerformanceObserver: PerformanceObserver
930})
931```
932
933### Middlewares
934
935Middlewares are functions that can intercept requests before being processed by Fetch.
936Wretch includes a helper to help replicate the [middleware](http://expressjs.com/en/guide/using-middleware.html) style.
937
938
939#### Middlewares package
940
941Check out [wretch-middlewares](https://github.com/elbywan/wretch-middlewares), the official collection of middlewares.
942
943#### Signature
944
945Basically a Middleware is a function having the following signature :
946
947```ts
948// A middleware accepts options and returns a configured version
949type Middleware = (options?: {[key: string]: any}) => ConfiguredMiddleware
950// A configured middleware (with options curried)
951type ConfiguredMiddleware = (next: FetchLike) => FetchLike
952// A "fetch like" function, accepting an url and fetch options and returning a response promise
953type FetchLike = (url: string, opts: WretcherOptions) => Promise<WretcherResponse>
954```
955
956#### middlewares(middlewares: ConfiguredMiddleware[], clear = false)
957
958Add middlewares to intercept a request before being sent.
959
960```javascript
961/* A simple delay middleware. */
962const delayMiddleware = delay => next => (url, opts) => {
963 return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
964}
965
966// The request will be delayed by 1 second.
967wretch("...").middlewares([
968 delayMiddleware(1000)
969]).get().res(_ => /* ... */)
970```
971
972#### Middleware examples
973
974```javascript
975/* A simple delay middleware. */
976const delayMiddleware = delay => next => (url, opts) => {
977 return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
978}
979
980/* Returns the url and method without performing an actual request. */
981const shortCircuitMiddleware = () => next => (url, opts) => {
982 // We create a new Response object to comply because wretch expects that from fetch.
983 const response = new Response()
984 response.text = () => Promise.resolve(opts.method + "@" + url)
985 response.json = () => Promise.resolve({ url, method: opts.method })
986 // Instead of calling next(), returning a Response Promise bypasses the rest of the chain.
987 return Promise.resolve(response)
988}
989
990/* Logs all requests passing through. */
991const logMiddleware = () => next => (url, opts) => {
992 console.log(opts.method + "@" + url)
993 return next(url, opts)
994}
995
996/* A throttling cache. */
997const cacheMiddleware = (throttle = 0) => {
998
999 const cache = new Map()
1000 const inflight = new Map()
1001 const throttling = new Set()
1002
1003 return next => (url, opts) => {
1004 const key = opts.method + "@" + url
1005
1006 if(!opts.noCache && throttling.has(key)) {
1007 // If the cache contains a previous response and we are throttling, serve it and bypass the chain.
1008 if(cache.has(key))
1009 return Promise.resolve(cache.get(key).clone())
1010 // If the request in already in-flight, wait until it is resolved
1011 else if(inflight.has(key)) {
1012 return new Promise((resolve, reject) => {
1013 inflight.get(key).push([resolve, reject])
1014 })
1015 }
1016 }
1017
1018 // Init. the pending promises Map
1019 if(!inflight.has(key))
1020 inflight.set(key, [])
1021
1022 // If we are not throttling, activate the throttle for X milliseconds
1023 if(throttle && !throttling.has(key)) {
1024 throttling.add(key)
1025 setTimeout(() => { throttling.delete(key) }, throttle)
1026 }
1027
1028 // We call the next middleware in the chain.
1029 return next(url, opts)
1030 .then(_ => {
1031 // Add a cloned response to the cache
1032 cache.set(key, _.clone())
1033 // Resolve pending promises
1034 inflight.get(key).forEach((([resolve, reject]) => resolve(_.clone()))
1035 // Remove the inflight pending promises
1036 inflight.delete(key)
1037 // Return the original response
1038 return _
1039 })
1040 .catch(_ => {
1041 // Reject pending promises on error
1042 inflight.get(key).forEach(([resolve, reject]) => reject(_))
1043 inflight.delete(key)
1044 throw _
1045 })
1046 }
1047}
1048
1049// To call a single middleware
1050const cache = cacheMiddleware(1000)
1051wretch("...").middlewares([cache]).get()
1052
1053// To chain middlewares
1054wretch("...").middlewares([
1055 logMiddleware(),
1056 delayMiddleware(1000),
1057 shortCircuitMiddleware()
1058}).get().text(_ => console.log(text))
1059
1060// To test the cache middleware more thoroughly
1061const wretchCache = wretch().middlewares([cacheMiddleware(1000)])
1062const printResource = (url, timeout = 0) =>
1063 setTimeout(_ => wretchCache.url(url).get().notFound(console.error).text(console.log), timeout)
1064// The resource url, change it to an invalid route to check the error handling
1065const resourceUrl = "/"
1066// Only two actual requests are made here even though there are 30 calls
1067for(let i = 0; i < 10; i++) {
1068 printResource(resourceUrl)
1069 printResource(resourceUrl, 500)
1070 printResource(resourceUrl, 1500)
1071}
1072```
1073
1074# License
1075
1076MIT