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

Added AbortController polyfill #572

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
"worker": true,
"globals": {
"JSON": false,
"URLSearchParams": false
"URLSearchParams": false,
"AbortController": false,
"AbortSignal": false,
"DOMException": false,
"Event": false
}
}
116 changes: 116 additions & 0 deletions fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,119 @@
}
self.fetch.polyfill = true
})(typeof self !== 'undefined' ? self : this);
(function(self) {
'use strict';

if (self.AbortController) {
return
}

function Emitter() {
this.listeners = {}
}

Emitter.prototype.listeners = null
Emitter.prototype.addEventListener = function(type, callback) {
if (!(type in this.listeners)) {
this.listeners[type] = []
}
this.listeners[type].push(callback)
}

Emitter.prototype.removeEventListener = function(type, callback) {
if (!(type in this.listeners)) {
return
}
var stack = this.listeners[type]
for (var i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback){
stack.splice(i, 1)
return
}
}
}

Emitter.prototype.dispatchEvent = function(event) {
if (!(event.type in this.listeners)) {
return true
}
var stack = this.listeners[event.type]

for (var i = 0, l = stack.length; i < l; i++) {
stack[i].call(this, event)
}
return !event.defaultPrevented
}

function AbortSignal() {
Emitter.call(this)
this.aborted = false
}

AbortSignal.prototype = Object.create(Emitter.prototype)
AbortSignal.prototype.constructor = AbortSignal

AbortSignal.prototype.toString = function () {
return '[object AbortSignal]'
}


function AbortController() {
this.signal = new AbortSignal()
}

AbortController.prototype.abort = function() {
this.signal.aborted = true
this.signal.dispatchEvent(new Event('abort'))
}

AbortController.prototype.toString = function() {
return '[object AbortController]'
}

if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
// These are necessary to make sure that we get correct output for:
// Object.prototype.toString.call(new AbortController())
AbortController.prototype[Symbol.toStringTag] = 'AbortController'
AbortSignal.prototype[Symbol.toStringTag] = 'AbortSignal'
}

var realFetch = fetch
var abortableFetch = function(input, init) {
if (init && init.signal) {
var abortError
try {
abortError = new DOMException('Aborted', 'AbortError')
} catch (err) {
// IE 11 does not support calling the DOMException constructor, use a
// regular error object on it instead.
abortError = new Error('Aborted')
abortError.name = 'AbortError'
}

// Return early if already aborted, thus avoiding making an HTTP request
if (init.signal.aborted) {
return Promise.reject(abortError)
}

// Turn an event into a promise, reject it once `abort` is dispatched
var cancellation = new Promise(function(_, reject) {
init.signal.addEventListener('abort', function() {
reject(abortError)
}, {once: true});
})

delete init.signal

// Return the fastest promise (don't need to wait for request to finish)
return Promise.race([cancellation, realFetch(input, init)])
}

return realFetch(input, init)
};

self.fetch = abortableFetch
self.AbortController = AbortController
self.AbortSignal = AbortSignal

})(typeof self !== 'undefined' ? self : this);
5 changes: 5 additions & 0 deletions script/server
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ var routes = {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end('not json {');
},
'/testAbort': function(res, req) {
setTimeout(function() {
res.end();
}, 200);
},
'/cookie': function(res, req) {
var setCookie, cookie
var params = querystring.parse(url.parse(req.url).query);
Expand Down
98 changes: 98 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1192,5 +1192,103 @@ suite('credentials mode', function() {
})
})

suite('abort tests', function() {

test('abort during fetch', function() {
var controller = new AbortController()

setTimeout(function () {
controller.abort()
})

return fetch('/testAbort', {
signal: controller.signal
}).then(function () {
assert(false, 'Fetch should have been aborted')
}).catch(function (err) {
assert.equal(err.name, 'AbortError')
})
})

test('abort before fetch started', function() {
var controller = new AbortController()
controller.abort()

return fetch('/testAbort', {
signal: controller.signal
}).then(function () {
assert(false, 'Fetch should have been aborted')
}).catch(function (err) {
assert.equal(err.name, 'AbortError')
})
})

test('fetch without aborting', function() {
var controller = new AbortController()

return fetch('/testAbort', {
signal: controller.signal
}).catch(function () {
assert(false, 'Fetch should not have been aborted')
})
})

test('fetch without signal set', function() {
return fetch('/testAbort').catch(function () {
assert(false, 'Fetch should not have been aborted')
})
})

test('event listener fires "abort" event', function() {
return new Promise(function (resolve) {
var controller = new AbortController()
controller.signal.addEventListener('abort', function () {
resolve()
})
controller.abort()
})
})

test('signal.aborted is true after abort', function() {
return new Promise(function (resolve, reject) {
var controller = new AbortController()
controller.signal.addEventListener('abort', function () {
if (controller.signal.aborted === true) {
resolve()
} else {
reject()
}
})
controller.abort()
if (controller.signal.aborted !== true) {
reject()
}
})
})

test('event listener doesn\'t fire "abort" event after removeEventListener', function() {
return new Promise(function(resolve, reject) {
var controller = new AbortController()
controller.signal.addEventListener('abort', reject)
controller.signal.removeEventListener('abort', reject)
controller.abort()
resolve()
})
})


test('toString() output', function() {
assert.equal(new AbortController().toString(), '[object AbortController]')
assert.equal(new AbortController().signal.toString(), '[object AbortSignal]')
assert.equal(new AbortSignal().toString(), '[object AbortSignal]')

if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
assert.equal(Object.prototype.toString.call(new AbortController()), '[object AbortController]')
assert.equal(Object.prototype.toString.call(new AbortSignal()), '[object AbortSignal]')
}

})
})

})
})