From 19aaedc6f502fc92d9770e1e196bafdd6a5dd87f Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 31 May 2018 12:52:37 -0500 Subject: [PATCH 1/3] add support for signal request option fixes #390 add support for a signal: AbortSignal property in the request options When provided, the request can be aborted by calling the AbortController#abort method, similar to the fetch API --- src/request/interfaces.d.ts | 5 +++++ src/request/providers/node.ts | 4 ++++ src/request/providers/xhr.ts | 4 ++++ tests/unit/request/node.ts | 16 ++++++++++++++++ tests/unit/request/xhr.ts | 19 +++++++++++++++++++ 5 files changed, 48 insertions(+) diff --git a/src/request/interfaces.d.ts b/src/request/interfaces.d.ts index c38fd22e..7c5e2ef3 100644 --- a/src/request/interfaces.d.ts +++ b/src/request/interfaces.d.ts @@ -1,3 +1,4 @@ +import { AbortSignal } from '@dojo/shim/AbortController'; import { IterableIterator } from '@dojo/shim/iterator'; import Task from '../async/Task'; import UrlSearchParams, { ParamList } from '../UrlSearchParams'; @@ -57,6 +58,10 @@ export interface RequestOptions { * Password for HTTP authentication */ password?: string; + /** + * Abort signal, allowing for cancelable requests + */ + signal?: AbortSignal; /** * Number of milliseconds before the request times out and is canceled */ diff --git a/src/request/providers/node.ts b/src/request/providers/node.ts index 94b127d9..16d4c14a 100644 --- a/src/request/providers/node.ts +++ b/src/request/providers/node.ts @@ -671,6 +671,10 @@ export default function node(url: string, options: NodeRequestOptions = {}): Upl throw error; }); + if (options.signal) { + options.signal.addEventListener('abort', () => requestTask.cancel()); + } + requestTask.upload = new Observable((observer) => uploadObserverPool.add(observer)); return requestTask; diff --git a/src/request/providers/xhr.ts b/src/request/providers/xhr.ts index 333bb767..fac3080c 100644 --- a/src/request/providers/xhr.ts +++ b/src/request/providers/xhr.ts @@ -263,6 +263,10 @@ export default function xhr(url: string, options: XhrRequestOptions = {}): Uploa setOnError(request, reject); }, abort); + if (options.signal) { + options.signal.addEventListener('abort', () => task.cancel()); + } + request.open(options.method, requestUrl, !options.blockMainThread, options.user, options.password); if (has('filereader') && has('blob')) { diff --git a/tests/unit/request/node.ts b/tests/unit/request/node.ts index c7548bea..42a905ed 100644 --- a/tests/unit/request/node.ts +++ b/tests/unit/request/node.ts @@ -7,6 +7,8 @@ import * as zlib from 'zlib'; import { Response } from '../../../src/request/interfaces'; import { default as nodeRequest, NodeResponse } from '../../../src/request/providers/node'; import TimeoutError from '../../../src/request/TimeoutError'; +import { State } from '../../../src/async/Task'; +import AbortController from '@dojo/shim/AbortController'; const serverPort = 8124; const serverUrl = 'http://localhost:' + serverPort; @@ -463,6 +465,20 @@ registerSuite('request/node', { ); }, + signal(this: any) { + const dfd = this.async(); + const url = getRequestUrl('foo.json'); + const controller = new AbortController(); + const { signal } = controller; + const request = nodeRequest(url, { signal }); + request.finally( + dfd.callback(() => { + assert.strictEqual(request.state, State.Canceled); + }) + ); + controller.abort(); + }, + 'user and password': { both(this: any): void { const user = 'user name'; diff --git a/tests/unit/request/xhr.ts b/tests/unit/request/xhr.ts index 1b575b0d..47502e9e 100644 --- a/tests/unit/request/xhr.ts +++ b/tests/unit/request/xhr.ts @@ -1,11 +1,13 @@ const { registerSuite } = intern.getInterface('object'); const { assert } = intern.getPlugin('chai'); +import AbortController from '@dojo/shim/AbortController'; import xhrRequest, { XhrResponse } from '../../../src/request/providers/xhr'; import { Response } from '../../../src/request/interfaces'; import UrlSearchParams from '../../../src/UrlSearchParams'; import has from '../../../src/has'; import Promise from '@dojo/shim/Promise'; +import { State } from '../../../src/async/Task'; let echoServerAvailable = false; registerSuite('request/providers/xhr', { @@ -94,6 +96,23 @@ registerSuite('request/providers/xhr', { ); }, + '"signal"'(this: any) { + if (!echoServerAvailable) { + this.skip('No echo server available'); + } + const dfd = this.async(); + const controller = new AbortController(); + const { signal } = controller; + const request = xhrRequest('/__echo/foo.json', { signal, timeout: 100 }); + request.finally( + dfd.callback(() => { + assert.strictEqual(request.state, State.Canceled); + }) + ); + + controller.abort(); + }, + 'user and password'(this: any) { if (!echoServerAvailable) { this.skip('No echo server available'); From b0410d0c889cd6474daf30f344c1a8739cd8b7f5 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 31 May 2018 17:06:04 -0500 Subject: [PATCH 2/3] update to reject with AbortError over canceling When abort is triggered, update the request Task to reject with a AbortError instead of canceling the Task. Tasks can still be canceled with the .cancel method. --- src/request/providers/node.ts | 13 +++++++++---- src/request/providers/xhr.ts | 17 +++++++++++++---- tests/unit/request/node.ts | 7 +++---- tests/unit/request/xhr.ts | 7 +++---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/request/providers/node.ts b/src/request/providers/node.ts index 16d4c14a..01d78c92 100644 --- a/src/request/providers/node.ts +++ b/src/request/providers/node.ts @@ -654,6 +654,15 @@ export default function node(url: string, options: NodeRequestOptions = {}): Upl timeoutReject && timeoutReject(new TimeoutError('The request timed out')); }, options.timeout); } + + if (options.signal) { + options.signal.addEventListener('abort', () => { + const abortError = new Error('Aborted'); + abortError.name = 'AbortError'; + reject(abortError); + }); + } + }, () => { request.abort(); @@ -671,10 +680,6 @@ export default function node(url: string, options: NodeRequestOptions = {}): Upl throw error; }); - if (options.signal) { - options.signal.addEventListener('abort', () => requestTask.cancel()); - } - requestTask.upload = new Observable((observer) => uploadObserverPool.add(observer)); return requestTask; diff --git a/src/request/providers/xhr.ts b/src/request/providers/xhr.ts index fac3080c..75a7f047 100644 --- a/src/request/providers/xhr.ts +++ b/src/request/providers/xhr.ts @@ -205,6 +205,19 @@ export default function xhr(url: string, options: XhrRequestOptions = {}): Uploa const task = >new Task((resolve, reject) => { timeoutReject = reject; + if (options.signal) { + options.signal.addEventListener('abort', () => { + let abortError: Error; + try { + abortError = new DOMException('Aborted', 'AbortError'); + } catch { + abortError = new Error('Aborted'); + abortError.name = 'AbortError'; + } + reject(new DOMException('Aborted', 'AbortError')); + }); + } + request.onreadystatechange = function() { if (isAborted) { return; @@ -263,10 +276,6 @@ export default function xhr(url: string, options: XhrRequestOptions = {}): Uploa setOnError(request, reject); }, abort); - if (options.signal) { - options.signal.addEventListener('abort', () => task.cancel()); - } - request.open(options.method, requestUrl, !options.blockMainThread, options.user, options.password); if (has('filereader') && has('blob')) { diff --git a/tests/unit/request/node.ts b/tests/unit/request/node.ts index 42a905ed..a4f79658 100644 --- a/tests/unit/request/node.ts +++ b/tests/unit/request/node.ts @@ -7,7 +7,6 @@ import * as zlib from 'zlib'; import { Response } from '../../../src/request/interfaces'; import { default as nodeRequest, NodeResponse } from '../../../src/request/providers/node'; import TimeoutError from '../../../src/request/TimeoutError'; -import { State } from '../../../src/async/Task'; import AbortController from '@dojo/shim/AbortController'; const serverPort = 8124; @@ -471,9 +470,9 @@ registerSuite('request/node', { const controller = new AbortController(); const { signal } = controller; const request = nodeRequest(url, { signal }); - request.finally( - dfd.callback(() => { - assert.strictEqual(request.state, State.Canceled); + request.catch( + dfd.callback((error: Error) => { + assert.strictEqual(error.name, 'AbortError'); }) ); controller.abort(); diff --git a/tests/unit/request/xhr.ts b/tests/unit/request/xhr.ts index 47502e9e..5f22c87f 100644 --- a/tests/unit/request/xhr.ts +++ b/tests/unit/request/xhr.ts @@ -7,7 +7,6 @@ import { Response } from '../../../src/request/interfaces'; import UrlSearchParams from '../../../src/UrlSearchParams'; import has from '../../../src/has'; import Promise from '@dojo/shim/Promise'; -import { State } from '../../../src/async/Task'; let echoServerAvailable = false; registerSuite('request/providers/xhr', { @@ -104,9 +103,9 @@ registerSuite('request/providers/xhr', { const controller = new AbortController(); const { signal } = controller; const request = xhrRequest('/__echo/foo.json', { signal, timeout: 100 }); - request.finally( - dfd.callback(() => { - assert.strictEqual(request.state, State.Canceled); + request.catch( + dfd.callback((error: Error) => { + assert.strictEqual(error.name, 'AbortError'); }) ); From 4b6375124c082831f331d3b634d40eaeb58823f6 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 31 May 2018 18:08:50 -0500 Subject: [PATCH 3/3] correct xhr to use abortError --- src/request/providers/xhr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/request/providers/xhr.ts b/src/request/providers/xhr.ts index 75a7f047..34661fa2 100644 --- a/src/request/providers/xhr.ts +++ b/src/request/providers/xhr.ts @@ -214,7 +214,7 @@ export default function xhr(url: string, options: XhrRequestOptions = {}): Uploa abortError = new Error('Aborted'); abortError.name = 'AbortError'; } - reject(new DOMException('Aborted', 'AbortError')); + reject(abortError); }); }