From 0ddf55f79a87d50f0b5b3a8498e3699aa62b794c Mon Sep 17 00:00:00 2001 From: Vadim Khamzin Date: Tue, 17 Oct 2017 16:46:42 +0400 Subject: [PATCH 1/2] Added AbortController polyfill --- .jshintrc | 5 ++- fetch.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++ script/server | 5 +++ test/test.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 1 deletion(-) diff --git a/.jshintrc b/.jshintrc index c451b546..ba5fc386 100644 --- a/.jshintrc +++ b/.jshintrc @@ -20,6 +20,9 @@ "worker": true, "globals": { "JSON": false, - "URLSearchParams": false + "URLSearchParams": false, + "AbortController": false, + "AbortSignal": false, + "DOMException": false } } diff --git a/fetch.js b/fetch.js index f2f466d7..d3a6b772 100644 --- a/fetch.js +++ b/fetch.js @@ -464,3 +464,93 @@ } self.fetch.polyfill = true })(typeof self !== 'undefined' ? self : this); +(function(self) { + 'use strict'; + + if (self.AbortController) { + return + } + + function Emitter() { + var delegate = document.createDocumentFragment() + var methods = ['addEventListener', 'dispatchEvent', 'removeEventListener'] + methods.forEach(function(method) { + this[method] = function () { + delegate[method].apply(delegate, arguments); + }; + }.bind(this)) + } + + + 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); \ No newline at end of file diff --git a/script/server b/script/server index 00993f84..dcc24165 100755 --- a/script/server +++ b/script/server @@ -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); diff --git a/test/test.js b/test/test.js index 0db2307f..71694d97 100644 --- a/test/test.js +++ b/test/test.js @@ -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]') + } + + }) +}) + }) }) From 1b65c0323171aa78bc875ecd0cedab195702b4a7 Mon Sep 17 00:00:00 2001 From: Vadim Khamzin Date: Tue, 17 Oct 2017 18:03:20 +0400 Subject: [PATCH 2/2] Fixed web workers --- .jshintrc | 3 ++- fetch.js | 40 +++++++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.jshintrc b/.jshintrc index ba5fc386..7e0819c7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -23,6 +23,7 @@ "URLSearchParams": false, "AbortController": false, "AbortSignal": false, - "DOMException": false + "DOMException": false, + "Event": false } } diff --git a/fetch.js b/fetch.js index d3a6b772..87f255f8 100644 --- a/fetch.js +++ b/fetch.js @@ -472,15 +472,41 @@ } function Emitter() { - var delegate = document.createDocumentFragment() - var methods = ['addEventListener', 'dispatchEvent', 'removeEventListener'] - methods.forEach(function(method) { - this[method] = function () { - delegate[method].apply(delegate, arguments); - }; - }.bind(this)) + 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)