developit / developit/unfetch

The second alternative to AbortController support (#54, #68, #137)

Abierto
#148 0 comentarios 0 reacciones 0 asignados Ver en GitHub
Lenguaje dominante
JavaScript
Estrellas
5.7k
Forks
204
Métricas de merge de PR
Sin PR fusionados en 30 d

Descripción

Resolves #54
Alternative to #68, and #137

### This solution
- Supports single `abort()` for the several parallel fetch requests
- Actually do http request abort over the network (not wiaitng for the result and ignores it like with `Promise.race` surrogates)
- Based on standard `AbortController` implementation (can run with polyfills on IE, see below)
- Do not run request if it was aborted already
- Do not abort request if it was complete (`readyState === DONE_STATE`)
- Add credentials not only for `include` but for `same-origin` also (usefull for some CORS use cases)
- Accumulates #54, #68, #137 discussions, [this](https://github.com/stereobooster/react-ideal-image/blob/master/src/components/unfetch.js) `unfetch` enchancement and [this](https://codesandbox.io/s/92vnjok08r?file=/src/xhr/xhr.js:316-330) implementation alternative

Not sure about **500 bytes** of the bundle size, but should be very close to it :wink:

### The code (`abortable-unfetch.mjs`)
```js
export default function (url, { method, headers, credentials, body, signal } = {}) {
return new Promise((resolve, reject) => {
const abortError = () => {
try {
return new DOMException('Aborted', 'AbortError')
} catch (error) { /* the DOMException constructor is not supported */
const abortError = new Error('Aborted')
abortError.name = 'AbortError'
return abortError
}
}

if (signal && signal.aborted) {
reject(abortError())
return
}

const request = new XMLHttpRequest()
const keys = []
const all = []
const respHeaders = {}

const response = () => ({
ok: (request.status / 100 | 0) === 2, // 200-299
statusText: request.statusText,
status: request.status,
url: request.responseURL,
text: () => Promise.resolve(request.responseText),
json: () => Promise.resolve(request.responseText).then(JSON.parse),
blob: () => Promise.resolve(new Blob([request.response])),
clone: response,
headers: {
keys: () => keys,
entries: () => all,
get: n => respHeaders[n.toLowerCase()],
has: n => n.toLowerCase() in respHeaders,
},
})

request.open(method || 'get', url, true)

request.onload = () => {
request.getAllResponseHeaders().
replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,
(m, key, value) => {
keys.push(key = key.toLowerCase())
all.push([key, value])
respHeaders[key] = respHeaders[key]
? `${respHeaders[key]},${value}`
: value
})
resolve(response())
}

if (signal) {
const abortListener = () => request.abort()
signal.addEventListener('abort', abortListener)
request.onreadystatechange = () => {
if (request.readyState === 4) { /* DONE_STATE = 4 */
signal.removeEventListener('abort', abortListener)
}
}
}
request.onabort = () => reject(abortError())
request.onerror = reject

request.withCredentials = credentials === 'same-origin' || credentials === 'include'

for (const i in headers) {
request.setRequestHeader(i, headers[i])
}

request.send(body || null)
})
}
```

### The polyfill (`abortable-unfetch-polyfill.mjs`)
```js
import abortableUnfetch from './abortable-unfetch'

const g =
typeof self !== 'undefined' ? self :
typeof window !== 'undefined' ? window :
typeof global !== 'undefined' ? global :
undefined

if (g) {
if (typeof g.fetch === 'undefined') {
g.fetch = abortableUnfetch
}
}
```

### `AbortController` polyfills

There are two alternatives to polyfill the standard `AbortController` behaviour on the old browsers (like IE11):
- [`abortcontroller-polyfill`](https://github.com/mo/abortcontroller-polyfill)
- [`abort-controller`](https://github.com/mysticatea/abort-controller#readme)

For the minimum bundle size I prefer the first one and because we based on the standard AbortController API we do not need to polyfill fetch additionally:
```sh
npm i abortcontroller-polyfill
yarn add abortcontroller-polyfill
pnpm add abortcontroller-polyfill
```
Then somewhere in your `index.mjs`:
```js
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
import './abortable-unfetch-polyfill'
```
Do not forget to exclude transpiled polyfill code from the Babel transpile:
`exclude: [ "node_modules/**" ],` or `exclude: [ "node_modules/abortcontroller-polyfill/**" ],`

### Usage example
```js
let abortController

// abort can be called as many times as you want (it run only ones)
const abort = () => abortController && abortController.abort()

const doFetch = () => {

abort() // abort the previous / ongoing call
abortController = new AbortController()

fetch('http://api.plos.org/search?q=title:DNA',
{ credentials: 'same-origin', signal: abortController.signal }).
then(response => {
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`)
}
return response.json()
}).
then(data => {
console.log('REQUEST FINISHED')
console.dir(data)
}).
catch(error => {
if (error.name === 'AbortError') {
console.log('REQUEST ABORTED')
} else {
console.error(`REQUEST FAILED: ${error ? error.message : ''}`)
}

/* The alternative way to distinct the AbortError */
if (abortController.signal.aborted) {
console.log('REQUEST ABORTED')
} else {
console.error(`REQUEST FAILED: ${error ? error.message : ''}`)
}
})
}

doFetch()
setTimeout(abort, 5000)
```

P.S. @developit, @prk3, @simonbuerger, @prabirshrestha PR, tests and discussion are welcome :smile:

Guía de contribución

No hay ninguna guía de contribución indexada para este repositorio

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.