From 93e9cec6c6a93f7a9473381c6daf215172fc6b7c Mon Sep 17 00:00:00 2001 From: Jmeas Date: Thu, 7 Dec 2017 01:18:19 -0800 Subject: [PATCH 01/24] Support abort API --- fetch.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/fetch.js b/fetch.js index f2f466d7..4e02c2db 100644 --- a/fetch.js +++ b/fetch.js @@ -1,7 +1,21 @@ (function(self) { 'use strict'; - if (self.fetch) { + function canAbortFetch() { + if (!self.AbortController || !self.AbortSignal) { + return false + } + + var abortController = new self.AbortController() + + var request = new self.Request('http://a', { + signal: abortController.signal + }) + + return Boolean(request.signal) + } + + if (self.fetch && canAbortFetch()) { return } @@ -443,6 +457,10 @@ reject(new TypeError('Network request failed')) } + xhr.onabort = function() { + reject(new DOMException('Aborted', 'AbortError')) + } + xhr.open(request.method, request.url, true) if (request.credentials === 'include') { @@ -459,6 +477,10 @@ xhr.setRequestHeader(name, value) }) + if (init.signal && init.signal.addEventListener) { + init.signal.addEventListener('abort', xhr.abort) + } + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) }) } From 53565c1aec2a5ca026a58e0ce3460c654fc55a98 Mon Sep 17 00:00:00 2001 From: Jmeas Date: Thu, 7 Dec 2017 08:49:11 -0800 Subject: [PATCH 02/24] Support abort without replacing native fetch --- fetch.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/fetch.js b/fetch.js index 4e02c2db..41267405 100644 --- a/fetch.js +++ b/fetch.js @@ -1,21 +1,7 @@ (function(self) { 'use strict'; - function canAbortFetch() { - if (!self.AbortController || !self.AbortSignal) { - return false - } - - var abortController = new self.AbortController() - - var request = new self.Request('http://a', { - signal: abortController.signal - }) - - return Boolean(request.signal) - } - - if (self.fetch && canAbortFetch()) { + if (self.fetch) { return } From 9a7d232905f786b704651409e74b3d665adc2dac Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 7 Dec 2017 12:08:35 -0800 Subject: [PATCH 03/24] Remove event listener; use request.signal --- fetch.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fetch.js b/fetch.js index 41267405..5b42a839 100644 --- a/fetch.js +++ b/fetch.js @@ -385,6 +385,7 @@ this.statusText = 'statusText' in options ? options.statusText : 'OK' this.headers = new Headers(options.headers) this.url = options.url || '' + this.signal = options.signal this._initBody(bodyInit) } @@ -445,6 +446,7 @@ xhr.onabort = function() { reject(new DOMException('Aborted', 'AbortError')) + request.signal.removeEventListener('abort', xhr.abort) } xhr.open(request.method, request.url, true) @@ -463,8 +465,8 @@ xhr.setRequestHeader(name, value) }) - if (init.signal && init.signal.addEventListener) { - init.signal.addEventListener('abort', xhr.abort) + if (request.signal && request.signal.addEventListener) { + request.signal.addEventListener('abort', xhr.abort) } xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) From fd5c261bd12c1a5fb6fd6a24e9dd1697d5a82fd8 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 7 Dec 2017 12:11:08 -0800 Subject: [PATCH 04/24] Skip creation of XHR if signal is already aborted --- fetch.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fetch.js b/fetch.js index 5b42a839..fc6d1b4b 100644 --- a/fetch.js +++ b/fetch.js @@ -425,6 +425,10 @@ var request = new Request(input, init) var xhr = new XMLHttpRequest() + if (request.signal && request.signal.aborted) { + return reject(new DOMException('Aborted', 'AbortError')); + } + xhr.onload = function() { var options = { status: xhr.status, From 318e95f0609e5d5dc2d1f8d57263c04f154ee950 Mon Sep 17 00:00:00 2001 From: Jmeas Date: Thu, 7 Dec 2017 21:42:26 -0800 Subject: [PATCH 05/24] First pass at documentation --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 32cc3c20..dd0ed1e2 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ expected to uphold this code. * [Sending cookies](#sending-cookies) * [Receiving cookies](#receiving-cookies) * [Obtaining the Response URL](#obtaining-the-response-url) + * [Aborting requests](#aborting-requests) * [Browser Support](#browser-support) ## Read this first @@ -260,6 +261,47 @@ response.headers['X-Request-URL'] = request.url This server workaround is necessary if you need reliable `response.url` in Firefox < 32, Chrome < 37, Safari, or IE. +#### Aborting requests + +This polyfill supports +[the abortable fetch API](https://developers.google.com/web/updates/2017/09/abortable-fetch). +However, aborting a fetch requires use of two additional DOM APIs: +[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) +and +[AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). +Typically, browsers that do not support fetch will also not support +AbortController or AbortSignal. + +Consequently, you will need to find a polyfill for these APIs to abort fetches. + +Once you have an AbortController and AbortSignal polyfill in place, you can +abort a fetch like so: + +```js +const controller = new AbortController() + +fetch('/avatars', { + method: 'POST', + body: data, + signal: controller.signal +}) + .then( + function(response) { + console.log('request succeeded', response) + }, + function(ex) { + if (ex.name === 'AbortError') { + console.log('request aborted') + } else { + console.log('request failed, but it was not aborted') + } + } + ) + +// some time later... +controller.abort(); +``` + ## Browser Support - Chrome From 0e1b651f4492e31fb3b18297c574807fdb3aafa4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 8 Dec 2017 09:18:56 -0800 Subject: [PATCH 06/24] More tweaks based on feedback --- README.md | 23 ++++++----------------- fetch.js | 10 +++++++--- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index dd0ed1e2..a5958315 100644 --- a/README.md +++ b/README.md @@ -270,9 +270,8 @@ However, aborting a fetch requires use of two additional DOM APIs: and [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). Typically, browsers that do not support fetch will also not support -AbortController or AbortSignal. - -Consequently, you will need to find a polyfill for these APIs to abort fetches. +AbortController or AbortSignal. Consequently, you will need to include an +additional polyfill for these APIs to abort fetches. Once you have an AbortController and AbortSignal polyfill in place, you can abort a fetch like so: @@ -281,22 +280,12 @@ abort a fetch like so: const controller = new AbortController() fetch('/avatars', { - method: 'POST', - body: data, signal: controller.signal +}).catch(function(ex) { + if (ex.name === 'AbortError') { + console.log('request aborted') + } }) - .then( - function(response) { - console.log('request succeeded', response) - }, - function(ex) { - if (ex.name === 'AbortError') { - console.log('request aborted') - } else { - console.log('request failed, but it was not aborted') - } - } - ) // some time later... controller.abort(); diff --git a/fetch.js b/fetch.js index fc6d1b4b..a1d80373 100644 --- a/fetch.js +++ b/fetch.js @@ -429,6 +429,10 @@ return reject(new DOMException('Aborted', 'AbortError')); } + function abortXhr() { + xhr.abort(); + } + xhr.onload = function() { var options = { status: xhr.status, @@ -450,7 +454,7 @@ xhr.onabort = function() { reject(new DOMException('Aborted', 'AbortError')) - request.signal.removeEventListener('abort', xhr.abort) + request.signal.removeEventListener('abort', abortXhr) } xhr.open(request.method, request.url, true) @@ -469,8 +473,8 @@ xhr.setRequestHeader(name, value) }) - if (request.signal && request.signal.addEventListener) { - request.signal.addEventListener('abort', xhr.abort) + if (request.signal) { + request.signal.addEventListener('abort', abortXhr) } xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) From d47f6c9a7f92ee6248545a9e91e834a8443bafa6 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 8 Dec 2017 09:34:05 -0800 Subject: [PATCH 07/24] Remove some semicolons --- fetch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fetch.js b/fetch.js index a1d80373..1d4009a0 100644 --- a/fetch.js +++ b/fetch.js @@ -426,11 +426,11 @@ var xhr = new XMLHttpRequest() if (request.signal && request.signal.aborted) { - return reject(new DOMException('Aborted', 'AbortError')); + return reject(new DOMException('Aborted', 'AbortError')) } function abortXhr() { - xhr.abort(); + xhr.abort() } xhr.onload = function() { From 529c9fa0d1dcd88d8582aaf4a6a3b6b12d246d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 13 Dec 2017 15:13:08 +0100 Subject: [PATCH 08/24] Add `signal` property to Request instead of Response --- fetch.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fetch.js b/fetch.js index 1d4009a0..205f063c 100644 --- a/fetch.js +++ b/fetch.js @@ -317,6 +317,7 @@ } this.method = input.method this.mode = input.mode + this.signal = input.signal if (!body && input._bodyInit != null) { body = input._bodyInit input.bodyUsed = true @@ -331,6 +332,7 @@ } this.method = normalizeMethod(options.method || this.method || 'GET') this.mode = options.mode || this.mode || null + this.signal = options.signal || this.signal this.referrer = null if ((this.method === 'GET' || this.method === 'HEAD') && body) { @@ -385,7 +387,6 @@ this.statusText = 'statusText' in options ? options.statusText : 'OK' this.headers = new Headers(options.headers) this.url = options.url || '' - this.signal = options.signal this._initBody(bodyInit) } From 8ba0cdb9b3286d4e47ba239bd56e4ef7c05ae0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 13 Dec 2017 15:13:30 +0100 Subject: [PATCH 09/24] Polyfill DOMException if missing --- fetch.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fetch.js b/fetch.js index 205f063c..6ea03311 100644 --- a/fetch.js +++ b/fetch.js @@ -417,6 +417,12 @@ return new Response(null, {status: status, headers: {location: url}}) } + var DOMException = self.DOMException || function(message, name) { + var error = new Error(message) + error.name = name + return error + } + self.Headers = Headers self.Request = Request self.Response = Response From 074c89e2fc117f692b11c8b6b4fda55a330b600b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 13 Dec 2017 15:27:13 +0100 Subject: [PATCH 10/24] Polyfill DOMException In PhantomJS, DOMException exists but is not a constructor. --- fetch.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/fetch.js b/fetch.js index 6ea03311..555814a0 100644 --- a/fetch.js +++ b/fetch.js @@ -417,10 +417,18 @@ return new Response(null, {status: status, headers: {location: url}}) } - var DOMException = self.DOMException || function(message, name) { - var error = new Error(message) - error.name = name - return error + var DOMException = self.DOMException + try { + new DOMException() + } catch(err) { + DOMException = function(message, name) { + this.message = message + this.name = name + var error = Error(message) + this.stack = error.stack + } + DOMException.prototype = Object.create(Error.prototype) + DOMException.prototype.constructor = DOMException } self.Headers = Headers From b8322292e3147d3f9b9d30ccc6acebd8fbb51445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 13 Dec 2017 15:27:49 +0100 Subject: [PATCH 11/24] Add tests for aborting requests --- script/server | 6 ++++++ test/test.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/script/server b/script/server index 00993f84..753090f7 100755 --- a/script/server +++ b/script/server @@ -80,6 +80,12 @@ var routes = { res.writeHead(204); res.end(); }, + '/slow': function(res) { + setTimeout(function() { + res.writeHead(200); + res.end(); + }, 100); + }, '/error': function(res) { res.destroy(); }, diff --git a/test/test.js b/test/test.js index 0db2307f..f2fd7c50 100644 --- a/test/test.js +++ b/test/test.js @@ -952,6 +952,49 @@ suite('fetch method', function() { }) }) + suite('aborting', function() { + test('initially aborted signal', function() { + return fetch('/request', { + signal: {aborted: true} + }).then(function() { + assert.ok(false) + }, function(error) { + assert.equal(error.name, 'AbortError') + }) + }) + + test('mid-request', function() { + var signal = { + aborted: false, + addEventListener: function(name, fn) { + assert.equal(name, 'abort') + this._handler = fn + }, + removeEventListener: function(name, fn) { + assert.equal(name, 'abort') + if (this._handler === fn) { + delete this._handler + } + } + } + + setTimeout(function() { + if (signal._handler) { + signal._handler() + } + }, 30) + + return fetch('/slow', { + signal: signal + }).then(function() { + assert.ok(false) + }, function(error) { + assert.equal(error.name, 'AbortError') + assert.isUndefined(signal._handler) + }) + }) + }) + suite('response', function() { test('populates body', function() { return fetch('/hello').then(function(response) { From c6aff302f6fd42be9f058481ffdaf92e6a8d4a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 13 Dec 2017 15:33:20 +0100 Subject: [PATCH 12/24] Avoid creating XMLHttpRequest if we're immediately aborting --- fetch.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fetch.js b/fetch.js index 555814a0..cc760d0d 100644 --- a/fetch.js +++ b/fetch.js @@ -438,12 +438,13 @@ self.fetch = function(input, init) { return new Promise(function(resolve, reject) { var request = new Request(input, init) - var xhr = new XMLHttpRequest() if (request.signal && request.signal.aborted) { return reject(new DOMException('Aborted', 'AbortError')) } + var xhr = new XMLHttpRequest() + function abortXhr() { xhr.abort() } From d9aa590ecae34a5c3190615252a614a5faa565a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Fri, 2 Feb 2018 02:00:10 +0000 Subject: [PATCH 13/24] Clear event listener for all xhr completion status. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Vieira --- fetch.js | 8 +++++++- test/test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/fetch.js b/fetch.js index cc760d0d..072b897c 100644 --- a/fetch.js +++ b/fetch.js @@ -470,7 +470,6 @@ xhr.onabort = function() { reject(new DOMException('Aborted', 'AbortError')) - request.signal.removeEventListener('abort', abortXhr) } xhr.open(request.method, request.url, true) @@ -491,6 +490,13 @@ if (request.signal) { request.signal.addEventListener('abort', abortXhr) + + xhr.onreadystatechange = function() { + // DONE (success or failure) + if (xhr.readyState === 4) { + request.signal.removeEventListener('abort', abortXhr) + } + } } xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) diff --git a/test/test.js b/test/test.js index f2fd7c50..f2b2bcb8 100644 --- a/test/test.js +++ b/test/test.js @@ -993,6 +993,34 @@ suite('fetch method', function() { assert.isUndefined(signal._handler) }) }) + + test('does not leak memory', function() { + var signal = { + aborted: false, + addEventListener: function(name, fn) { + this._handler = fn + }, + removeEventListener: function(name, fn) { + if (this._handler === fn) { + delete this._handler + } + } + } + + // success + return fetch('/request', { + signal: signal + }).then(function() { + assert.isUndefined(signal._handler) + }).then(function () { + // failure + return fetch('/boom', { + signal: signal + }).catch(function() { + assert.isUndefined(signal._handler) + }) + }); + }) }) suite('response', function() { From 9f9a00fd8aba5a919cdff09c086ff71daeb39c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Fri, 2 Feb 2018 17:04:57 +0000 Subject: [PATCH 14/24] Test signal reuse. Add AbortSignal polyfill. --- .jshintrc | 3 +- test/abortsignal-polyfill.js | 68 +++++++++++++++++++++++++++++++ test/test.html | 1 + test/test.js | 78 +++++++++++++++++++++--------------- test/worker.js | 1 + 5 files changed, 118 insertions(+), 33 deletions(-) create mode 100644 test/abortsignal-polyfill.js diff --git a/.jshintrc b/.jshintrc index c451b546..d179472e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -20,6 +20,7 @@ "worker": true, "globals": { "JSON": false, - "URLSearchParams": false + "URLSearchParams": false, + "AbortSignal": false } } diff --git a/test/abortsignal-polyfill.js b/test/abortsignal-polyfill.js new file mode 100644 index 00000000..49d347da --- /dev/null +++ b/test/abortsignal-polyfill.js @@ -0,0 +1,68 @@ +// Simple AbortSignal polyfill used for testing. +(function(self) { + 'use strict'; + + if (self.AbortSignal) { + return + } + + // Polyfill EventTarget if not available. + self.EventTarget = self.EventTarget || (function () { + function EventTarget () { + this._listeners = {} + } + EventTarget.prototype.addEventListener = function(name, fn) { + if (!(name in this._listeners)) { + this._listeners[name] = [] + } + this._listeners[name].push(fn) + } + EventTarget.prototype.removeEventListener = function(name, fn) { + if (!(name in this._listeners)) { + return + } + var stack = this._listeners[name] + for (var i = 0; i < stack.length; i++) { + if (stack[i] === fn){ + stack.splice(i, 1) + return + } + } + } + EventTarget.prototype.dispatchEvent = function(event) { + if (!(event.type in this._listeners)) { + return + } + function debounce (fn) { + setTimeout(function () { + fn.call(this, event) + }, 0) + } + var stack = this._listeners[event.type] + for (var i = 0; i < stack.length; i++) { + debounce(stack[i]) + } + } + + return EventTarget + })(); + + function AbortSignal () { + self.EventTarget.call(this) + this.aborted = false + } + AbortSignal.prototype = Object.create(self.EventTarget.prototype) + AbortSignal.prototype.constructor = AbortSignal + + AbortSignal.prototype.dispatchEvent = function(event) { + if (event.type === 'abort'){ + this.aborted = true + if (typeof this.onabort === 'function') { + this.onabort.call(this, event) + } + } + self.EventTarget.prototype.dispatchEvent.call(this, event) + } + + self.AbortSignal = AbortSignal +})(typeof self !== 'undefined' ? self : this); \ No newline at end of file diff --git a/test/test.html b/test/test.html index 8a1e48f1..bbf24760 100644 --- a/test/test.html +++ b/test/test.html @@ -33,6 +33,7 @@ + diff --git a/test/test.js b/test/test.js index f2b2bcb8..30653fdd 100644 --- a/test/test.js +++ b/test/test.js @@ -954,8 +954,11 @@ suite('fetch method', function() { suite('aborting', function() { test('initially aborted signal', function() { + const signal = new AbortSignal() + signal.aborted = true + return fetch('/request', { - signal: {aborted: true} + signal: signal }).then(function() { assert.ok(false) }, function(error) { @@ -964,24 +967,10 @@ suite('fetch method', function() { }) test('mid-request', function() { - var signal = { - aborted: false, - addEventListener: function(name, fn) { - assert.equal(name, 'abort') - this._handler = fn - }, - removeEventListener: function(name, fn) { - assert.equal(name, 'abort') - if (this._handler === fn) { - delete this._handler - } - } - } + const signal = new AbortSignal() setTimeout(function() { - if (signal._handler) { - signal._handler() - } + signal.dispatchEvent({ type: 'abort' }) }, 30) return fetch('/slow', { @@ -990,36 +979,61 @@ suite('fetch method', function() { assert.ok(false) }, function(error) { assert.equal(error.name, 'AbortError') - assert.isUndefined(signal._handler) }) }) + test('abort multiple with same signal', function() { + const signal = new AbortSignal() + + setTimeout(function() { + signal.dispatchEvent({ type: 'abort' }) + }, 30) + + return Promise.all([ + fetch('/slow', { + signal: signal + }).then(function() { + assert.ok(false) + }, function(error) { + assert.equal(error.name, 'AbortError') + }), + fetch('/slow', { + signal: signal + }).then(function() { + assert.ok(false) + }, function(error) { + assert.equal(error.name, 'AbortError') + }) + ]) + }) + test('does not leak memory', function() { - var signal = { - aborted: false, - addEventListener: function(name, fn) { - this._handler = fn - }, - removeEventListener: function(name, fn) { - if (this._handler === fn) { - delete this._handler - } - } - } + const signal = new AbortSignal() // success return fetch('/request', { signal: signal }).then(function() { - assert.isUndefined(signal._handler) + assert.deepEqual(signal._listeners['abort'], []) }).then(function () { // failure return fetch('/boom', { signal: signal }).catch(function() { - assert.isUndefined(signal._handler) + assert.deepEqual(signal._listeners['abort'], []) }) - }); + }).then(function () { + // aborted + setTimeout(function() { + signal.dispatchEvent({ type: 'abort' }) + }, 30) + + return fetch('/slow', { + signal: signal + }).catch(function() { + assert.deepEqual(signal._listeners['abort'], []) + }) + }) }) }) diff --git a/test/worker.js b/test/worker.js index 025d0dd1..6feb0a33 100644 --- a/test/worker.js +++ b/test/worker.js @@ -5,6 +5,7 @@ mocha.setup('tdd') self.assert = chai.assert importScripts('/node_modules/promise-polyfill/promise.js') +importScripts('/test/abortsignal-polyfill.js') importScripts('/test/test.js') importScripts('/fetch.js') From bab0f3deaae80afe8e0d8fbf0dcc4aa597636adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Fri, 2 Feb 2018 18:12:45 +0000 Subject: [PATCH 15/24] Consistent implementation of EventTarget as in MDN. --- test/abortsignal-polyfill.js | 28 +++++++++++++++------------- test/test.js | 6 +++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/test/abortsignal-polyfill.js b/test/abortsignal-polyfill.js index 49d347da..8e892be4 100644 --- a/test/abortsignal-polyfill.js +++ b/test/abortsignal-polyfill.js @@ -9,28 +9,29 @@ // Polyfill EventTarget if not available. self.EventTarget = self.EventTarget || (function () { function EventTarget () { - this._listeners = {} + this.listeners = {} } - EventTarget.prototype.addEventListener = function(name, fn) { - if (!(name in this._listeners)) { - this._listeners[name] = [] + EventTarget.prototype.listeners = null; + EventTarget.prototype.addEventListener = function(type, callback) { + if (!(type in this.listeners)) { + this.listeners[type] = [] } - this._listeners[name].push(fn) + this.listeners[type].push(callback) } - EventTarget.prototype.removeEventListener = function(name, fn) { - if (!(name in this._listeners)) { + EventTarget.prototype.removeEventListener = function(type, callback) { + if (!(type in this.listeners)) { return } - var stack = this._listeners[name] - for (var i = 0; i < stack.length; i++) { - if (stack[i] === fn){ + var stack = this.listeners[type] + for (var i = 0, l = stack.length; i < l; i++) { + if (stack[i] === callback){ stack.splice(i, 1) return } } } EventTarget.prototype.dispatchEvent = function(event) { - if (!(event.type in this._listeners)) { + if (!(event.type in this.listeners)) { return } function debounce (fn) { @@ -38,10 +39,11 @@ fn.call(this, event) }, 0) } - var stack = this._listeners[event.type] - for (var i = 0; i < stack.length; i++) { + var stack = this.listeners[event.type] + for (var i = 0, l = stack.length; i < l; i++) { debounce(stack[i]) } + return !event.defaultPrevented } return EventTarget diff --git a/test/test.js b/test/test.js index 30653fdd..534ed600 100644 --- a/test/test.js +++ b/test/test.js @@ -1014,13 +1014,13 @@ suite('fetch method', function() { return fetch('/request', { signal: signal }).then(function() { - assert.deepEqual(signal._listeners['abort'], []) + assert.deepEqual(signal.listeners['abort'], []) }).then(function () { // failure return fetch('/boom', { signal: signal }).catch(function() { - assert.deepEqual(signal._listeners['abort'], []) + assert.deepEqual(signal.listeners['abort'], []) }) }).then(function () { // aborted @@ -1031,7 +1031,7 @@ suite('fetch method', function() { return fetch('/slow', { signal: signal }).catch(function() { - assert.deepEqual(signal._listeners['abort'], []) + assert.deepEqual(signal.listeners['abort'], []) }) }) }) From 0bed2bad5923280809c12e82662491fd85560a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Sat, 3 Feb 2018 12:03:09 +0000 Subject: [PATCH 16/24] Split EventTarget and AbortSignal polyfills. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Vieira --- .jshintrc | 1 + test/abortsignal-polyfill.js | 86 +++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/.jshintrc b/.jshintrc index d179472e..3c25f7fc 100644 --- a/.jshintrc +++ b/.jshintrc @@ -21,6 +21,7 @@ "globals": { "JSON": false, "URLSearchParams": false, + "EventTarget": false, "AbortSignal": false } } diff --git a/test/abortsignal-polyfill.js b/test/abortsignal-polyfill.js index 8e892be4..80f5da02 100644 --- a/test/abortsignal-polyfill.js +++ b/test/abortsignal-polyfill.js @@ -1,59 +1,65 @@ -// Simple AbortSignal polyfill used for testing. +// EventTarget Polyfill (function(self) { 'use strict'; - if (self.AbortSignal) { + if (self.EventTarget) { return } - // Polyfill EventTarget if not available. - self.EventTarget = self.EventTarget || (function () { - function EventTarget () { - this.listeners = {} + function EventTarget () { + this.listeners = {} + } + EventTarget.prototype.listeners = null; + EventTarget.prototype.addEventListener = function(type, callback) { + if (!(type in this.listeners)) { + this.listeners[type] = [] } - EventTarget.prototype.listeners = null; - EventTarget.prototype.addEventListener = function(type, callback) { - if (!(type in this.listeners)) { - this.listeners[type] = [] - } - this.listeners[type].push(callback) + this.listeners[type].push(callback) + } + EventTarget.prototype.removeEventListener = function(type, callback) { + if (!(type in this.listeners)) { + return } - EventTarget.prototype.removeEventListener = function(type, callback) { - if (!(type in this.listeners)) { + var stack = this.listeners[type] + for (var i = 0, l = stack.length; i < l; i++) { + if (stack[i] === callback){ + stack.splice(i, 1) 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 - } - } } - EventTarget.prototype.dispatchEvent = function(event) { - if (!(event.type in this.listeners)) { - return - } - function debounce (fn) { - setTimeout(function () { - fn.call(this, event) - }, 0) - } - var stack = this.listeners[event.type] - for (var i = 0, l = stack.length; i < l; i++) { - debounce(stack[i]) - } - return !event.defaultPrevented + } + EventTarget.prototype.dispatchEvent = function(event) { + if (!(event.type in this.listeners)) { + return } + function debounce (fn) { + setTimeout(function () { + fn.call(this, event) + }, 0) + } + var stack = this.listeners[event.type] + for (var i = 0, l = stack.length; i < l; i++) { + debounce(stack[i]) + } + return !event.defaultPrevented + } - return EventTarget - })(); + self.EventTarget = EventTarget +})(typeof self !== 'undefined' ? self : this); + +// AbortSignal polyfill +(function(self) { + 'use strict'; + + if (self.AbortSignal) { + return + } function AbortSignal () { - self.EventTarget.call(this) + EventTarget.call(this) this.aborted = false } - AbortSignal.prototype = Object.create(self.EventTarget.prototype) + AbortSignal.prototype = Object.create(EventTarget.prototype) AbortSignal.prototype.constructor = AbortSignal AbortSignal.prototype.dispatchEvent = function(event) { @@ -63,7 +69,7 @@ this.onabort.call(this, event) } } - self.EventTarget.prototype.dispatchEvent.call(this, event) + EventTarget.prototype.dispatchEvent.call(this, event) } self.AbortSignal = AbortSignal From 738d51a057478803dbeea41e0d369e3914ab3d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Sat, 3 Feb 2018 12:26:07 +0000 Subject: [PATCH 17/24] Add tests with signal within Request object. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Vieira --- test/test.js | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/test/test.js b/test/test.js index 534ed600..bb717d2b 100644 --- a/test/test.js +++ b/test/test.js @@ -954,7 +954,7 @@ suite('fetch method', function() { suite('aborting', function() { test('initially aborted signal', function() { - const signal = new AbortSignal() + var signal = new AbortSignal() signal.aborted = true return fetch('/request', { @@ -966,8 +966,21 @@ suite('fetch method', function() { }) }) + test('initially aborted signal within Request', function () { + var signal = new AbortSignal() + signal.aborted = true + + var request = new Request('/request', { signal: signal }) + + return fetch(request).then(function() { + assert.ok(false) + }, function(error) { + assert.equal(error.name, 'AbortError') + }) + }) + test('mid-request', function() { - const signal = new AbortSignal() + var signal = new AbortSignal() setTimeout(function() { signal.dispatchEvent({ type: 'abort' }) @@ -982,8 +995,23 @@ suite('fetch method', function() { }) }) + test('mid-request within Request', function() { + var signal = new AbortSignal() + var request = new Request('/slow', { signal: signal }) + + setTimeout(function() { + signal.dispatchEvent({ type: 'abort' }) + }, 30) + + return fetch(request).then(function() { + assert.ok(false) + }, function(error) { + assert.equal(error.name, 'AbortError') + }) + }) + test('abort multiple with same signal', function() { - const signal = new AbortSignal() + var signal = new AbortSignal() setTimeout(function() { signal.dispatchEvent({ type: 'abort' }) @@ -1008,7 +1036,7 @@ suite('fetch method', function() { }) test('does not leak memory', function() { - const signal = new AbortSignal() + var signal = new AbortSignal() // success return fetch('/request', { From 8f778f394316ea947264853dc61ba9df2be2ee6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Sat, 3 Feb 2018 17:54:29 +0000 Subject: [PATCH 18/24] Fix EventTarget callback invocation context. --- test/abortsignal-polyfill.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/abortsignal-polyfill.js b/test/abortsignal-polyfill.js index 80f5da02..061e3e02 100644 --- a/test/abortsignal-polyfill.js +++ b/test/abortsignal-polyfill.js @@ -32,9 +32,10 @@ if (!(event.type in this.listeners)) { return } + var self = this function debounce (fn) { setTimeout(function () { - fn.call(this, event) + fn.call(self, event) }, 0) } var stack = this.listeners[event.type] From f0e24f8ebb2d6f8e57c7ea69abfc3fac1ff4b36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Sat, 3 Feb 2018 19:04:19 +0000 Subject: [PATCH 19/24] Native EventTarget is not inheritable. --- .jshintrc | 1 - test/abortsignal-polyfill.js | 31 ++++++++++--------------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/.jshintrc b/.jshintrc index 3c25f7fc..d179472e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -21,7 +21,6 @@ "globals": { "JSON": false, "URLSearchParams": false, - "EventTarget": false, "AbortSignal": false } } diff --git a/test/abortsignal-polyfill.js b/test/abortsignal-polyfill.js index 061e3e02..1aed0ad4 100644 --- a/test/abortsignal-polyfill.js +++ b/test/abortsignal-polyfill.js @@ -1,22 +1,22 @@ -// EventTarget Polyfill +// AbortSignal polyfill (function(self) { 'use strict'; - if (self.EventTarget) { + if (self.AbortSignal) { return } - function EventTarget () { + function Emitter () { this.listeners = {} } - EventTarget.prototype.listeners = null; - EventTarget.prototype.addEventListener = function(type, callback) { + Emitter.prototype.listeners = null; + Emitter.prototype.addEventListener = function(type, callback) { if (!(type in this.listeners)) { this.listeners[type] = [] } this.listeners[type].push(callback) } - EventTarget.prototype.removeEventListener = function(type, callback) { + Emitter.prototype.removeEventListener = function(type, callback) { if (!(type in this.listeners)) { return } @@ -28,7 +28,7 @@ } } } - EventTarget.prototype.dispatchEvent = function(event) { + Emitter.prototype.dispatchEvent = function(event) { if (!(event.type in this.listeners)) { return } @@ -45,22 +45,11 @@ return !event.defaultPrevented } - self.EventTarget = EventTarget -})(typeof self !== 'undefined' ? self : this); - -// AbortSignal polyfill -(function(self) { - 'use strict'; - - if (self.AbortSignal) { - return - } - function AbortSignal () { - EventTarget.call(this) + Emitter.call(this) this.aborted = false } - AbortSignal.prototype = Object.create(EventTarget.prototype) + AbortSignal.prototype = Object.create(Emitter.prototype) AbortSignal.prototype.constructor = AbortSignal AbortSignal.prototype.dispatchEvent = function(event) { @@ -70,7 +59,7 @@ this.onabort.call(this, event) } } - EventTarget.prototype.dispatchEvent.call(this, event) + Emitter.prototype.dispatchEvent.call(this, event) } self.AbortSignal = AbortSignal From 36bf6925ca05792277b3ee4cc75aea3f14042882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Sun, 4 Feb 2018 15:06:07 +0000 Subject: [PATCH 20/24] Replace AbortSignal polyfill with abortcontroller-polyfill module. --- .jshintrc | 2 +- package.json | 1 + test/abortsignal-polyfill.js | 66 ------------------------------------ test/test.html | 2 +- test/test.js | 35 +++++++++---------- test/worker.js | 2 +- 6 files changed, 22 insertions(+), 86 deletions(-) delete mode 100644 test/abortsignal-polyfill.js diff --git a/.jshintrc b/.jshintrc index d179472e..8041a1ae 100644 --- a/.jshintrc +++ b/.jshintrc @@ -21,6 +21,6 @@ "globals": { "JSON": false, "URLSearchParams": false, - "AbortSignal": false + "AbortController": false } } diff --git a/package.json b/package.json index e6d80cc9..2f1c1e18 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "repository": "github/fetch", "license": "MIT", "devDependencies": { + "abortcontroller-polyfill": "1.1.1", "chai": "1.10.0", "jshint": "2.8.0", "mocha": "2.1.0", diff --git a/test/abortsignal-polyfill.js b/test/abortsignal-polyfill.js deleted file mode 100644 index 1aed0ad4..00000000 --- a/test/abortsignal-polyfill.js +++ /dev/null @@ -1,66 +0,0 @@ -// AbortSignal polyfill -(function(self) { - 'use strict'; - - if (self.AbortSignal) { - 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 - } - var self = this - function debounce (fn) { - setTimeout(function () { - fn.call(self, event) - }, 0) - } - var stack = this.listeners[event.type] - for (var i = 0, l = stack.length; i < l; i++) { - debounce(stack[i]) - } - return !event.defaultPrevented - } - - function AbortSignal () { - Emitter.call(this) - this.aborted = false - } - AbortSignal.prototype = Object.create(Emitter.prototype) - AbortSignal.prototype.constructor = AbortSignal - - AbortSignal.prototype.dispatchEvent = function(event) { - if (event.type === 'abort'){ - this.aborted = true - if (typeof this.onabort === 'function') { - this.onabort.call(this, event) - } - } - Emitter.prototype.dispatchEvent.call(this, event) - } - - self.AbortSignal = AbortSignal -})(typeof self !== 'undefined' ? self : this); \ No newline at end of file diff --git a/test/test.html b/test/test.html index bbf24760..9d4d82fb 100644 --- a/test/test.html +++ b/test/test.html @@ -33,7 +33,7 @@ - + diff --git a/test/test.js b/test/test.js index bb717d2b..4a102753 100644 --- a/test/test.js +++ b/test/test.js @@ -954,11 +954,11 @@ suite('fetch method', function() { suite('aborting', function() { test('initially aborted signal', function() { - var signal = new AbortSignal() - signal.aborted = true + var controller = new AbortController() + controller.abort() return fetch('/request', { - signal: signal + signal: controller.signal }).then(function() { assert.ok(false) }, function(error) { @@ -967,10 +967,10 @@ suite('fetch method', function() { }) test('initially aborted signal within Request', function () { - var signal = new AbortSignal() - signal.aborted = true + var controller = new AbortController() + controller.abort() - var request = new Request('/request', { signal: signal }) + var request = new Request('/request', { signal: controller.signal }) return fetch(request).then(function() { assert.ok(false) @@ -980,14 +980,14 @@ suite('fetch method', function() { }) test('mid-request', function() { - var signal = new AbortSignal() + var controller = new AbortController() setTimeout(function() { - signal.dispatchEvent({ type: 'abort' }) + controller.abort() }, 30) return fetch('/slow', { - signal: signal + signal: controller.signal }).then(function() { assert.ok(false) }, function(error) { @@ -996,11 +996,11 @@ suite('fetch method', function() { }) test('mid-request within Request', function() { - var signal = new AbortSignal() - var request = new Request('/slow', { signal: signal }) + var controller = new AbortController() + var request = new Request('/slow', { signal: controller.signal }) setTimeout(function() { - signal.dispatchEvent({ type: 'abort' }) + controller.abort() }, 30) return fetch(request).then(function() { @@ -1011,22 +1011,22 @@ suite('fetch method', function() { }) test('abort multiple with same signal', function() { - var signal = new AbortSignal() + var controller = new AbortController() setTimeout(function() { - signal.dispatchEvent({ type: 'abort' }) + controller.abort() }, 30) return Promise.all([ fetch('/slow', { - signal: signal + signal: controller.signal }).then(function() { assert.ok(false) }, function(error) { assert.equal(error.name, 'AbortError') }), fetch('/slow', { - signal: signal + signal: controller.signal }).then(function() { assert.ok(false) }, function(error) { @@ -1036,7 +1036,8 @@ suite('fetch method', function() { }) test('does not leak memory', function() { - var signal = new AbortSignal() + var controller = new AbortController() + var signal = controller.signal // success return fetch('/request', { diff --git a/test/worker.js b/test/worker.js index 6feb0a33..f18c938a 100644 --- a/test/worker.js +++ b/test/worker.js @@ -5,7 +5,7 @@ mocha.setup('tdd') self.assert = chai.assert importScripts('/node_modules/promise-polyfill/promise.js') -importScripts('/test/abortsignal-polyfill.js') +importScripts('/node_modules/abortcontroller-polyfill/dist/abortcontroller.js') importScripts('/test/test.js') importScripts('/fetch.js') From 7f6b8d2eb3e7d64cc3e8ec2c93332939ab5d859c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Vieira?= Date: Sun, 4 Feb 2018 18:41:17 +0000 Subject: [PATCH 21/24] Upgrade abortcontroller-polyfill. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f1c1e18..9684bff4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "repository": "github/fetch", "license": "MIT", "devDependencies": { - "abortcontroller-polyfill": "1.1.1", + "abortcontroller-polyfill": "1.1.2", "chai": "1.10.0", "jshint": "2.8.0", "mocha": "2.1.0", From f03586047c100c5e5171afc2f71a74492309c8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 23 May 2018 15:56:17 +0200 Subject: [PATCH 22/24] Scope abortcontroller to polyfill only --- package.json | 2 +- test/test.html | 2 +- test/worker.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8416e3dd..5153a7f4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "repository": "github/fetch", "license": "MIT", "devDependencies": { - "abortcontroller-polyfill": "1.1.2", + "abortcontroller-polyfill": "^1.1.9", "chai": "1.10.0", "eslint": "^4.19.1", "eslint-plugin-github": "^1.0.0", diff --git a/test/test.html b/test/test.html index 1d4f0c59..eea0df32 100644 --- a/test/test.html +++ b/test/test.html @@ -33,7 +33,7 @@ - + diff --git a/test/worker.js b/test/worker.js index 5cd1a36e..5bc87c41 100644 --- a/test/worker.js +++ b/test/worker.js @@ -5,7 +5,7 @@ mocha.setup('tdd') self.assert = chai.assert importScripts('/node_modules/promise-polyfill/promise.js') -importScripts('/node_modules/abortcontroller-polyfill/dist/abortcontroller.js') +importScripts('/node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js') importScripts('/dist/fetch.umd.js') importScripts('/test/test.js') From 2bd7f22032272dc5d0ab507da69b84060bf6804f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 23 May 2018 16:00:34 +0200 Subject: [PATCH 23/24] Export DOMException for `instanceof` checks --- fetch.js | 2 +- test/test.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/fetch.js b/fetch.js index cd4c660b..b684ff1b 100644 --- a/fetch.js +++ b/fetch.js @@ -424,7 +424,7 @@ Response.redirect = function(url, status) { return new Response(null, {status: status, headers: {location: url}}) } -var DOMException = self.DOMException +export var DOMException = self.DOMException try { new DOMException() } catch (err) { diff --git a/test/test.js b/test/test.js index 6bbb2f81..1308c63c 100644 --- a/test/test.js +++ b/test/test.js @@ -1060,6 +1060,7 @@ exercise.forEach(function(exerciseMode) { assert.ok(false) }, function(error) { + assert.instanceOf(error, WHATWGFetch.DOMException) assert.equal(error.name, 'AbortError') } ) From c873843dc8cbdb4d5af652a99476e90515133ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 23 May 2018 16:15:48 +0200 Subject: [PATCH 24/24] Polish documentation for aborting requests --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a5958315..ff3e5e7f 100644 --- a/README.md +++ b/README.md @@ -266,17 +266,17 @@ Firefox < 32, Chrome < 37, Safari, or IE. This polyfill supports [the abortable fetch API](https://developers.google.com/web/updates/2017/09/abortable-fetch). However, aborting a fetch requires use of two additional DOM APIs: -[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) -and +[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). Typically, browsers that do not support fetch will also not support -AbortController or AbortSignal. Consequently, you will need to include an -additional polyfill for these APIs to abort fetches. - -Once you have an AbortController and AbortSignal polyfill in place, you can -abort a fetch like so: +AbortController or AbortSignal. Consequently, you will need to include +[an additional polyfill](https://github.com/mo/abortcontroller-polyfill#readme) +for these APIs to abort fetches: ```js +import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only' +import {fetch} from 'whatwg-fetch' + const controller = new AbortController() fetch('/avatars', { @@ -288,7 +288,7 @@ fetch('/avatars', { }) // some time later... -controller.abort(); +controller.abort() ``` ## Browser Support