Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
eugeneilyin opened this issue Mar 25, 2021 · 0 comments
Open

Comments

@eugeneilyin
Copy link

eugeneilyin commented Mar 25, 2021

Resolves #54
Alternative to #68, and #137

This solution

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

The code (abortable-unfetch.mjs)

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)

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):

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:

npm i abortcontroller-polyfill
yarn add abortcontroller-polyfill
pnpm add abortcontroller-polyfill

Then somewhere in your index.mjs:

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

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 😄

@eugeneilyin eugeneilyin changed the title Second alternative to AbortController support (#54, #68, #137) The second alternative to AbortController support (#54, #68, #137) Mar 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant