Skip to content

Commit a88e6f3

Browse files
committed
refactor(http): use Observables in Http backends
BREAKING CHANGE: Http now returns Rx Observables directly, so calling .toRx() is no longer necessary. Additionally, Http calls are now cold, so backend requests will not fire unless .subscribe() is called. closes angular#4043 and closes angular#2974 Closes angular#4376
1 parent 3dd9919 commit a88e6f3

File tree

8 files changed

+162
-165
lines changed

8 files changed

+162
-165
lines changed

modules/angular2/src/http/backends/jsonp_backend.ts

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {BrowserJsonp} from './browser_jsonp';
88
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
99
import {makeTypeError} from 'angular2/src/core/facade/exceptions';
1010
import {StringWrapper, isPresent} from 'angular2/src/core/facade/lang';
11+
var Observable = require('@reactivex/rxjs/dist/cjs/Observable');
1112

1213
export class JSONPConnection implements Connection {
1314
readyState: ReadyStates;
1415
request: Request;
15-
response: EventEmitter;
16+
response: any;
1617
private _id: string;
1718
private _script: Element;
1819
private _responseData: any;
@@ -27,50 +28,66 @@ export class JSONPConnection implements Connection {
2728
throw makeTypeError("JSONP requests must use GET request method.");
2829
}
2930
this.request = req;
30-
this.response = new EventEmitter();
31-
this.readyState = ReadyStates.Loading;
32-
this._id = _dom.nextRequestID();
33-
34-
_dom.exposeConnection(this._id, this);
35-
36-
// Workaround Dart
37-
// url = url.replace(/=JSONP_CALLBACK(&|$)/, `generated method`);
38-
let callback = _dom.requestCallback(this._id);
39-
let url: string = req.url;
40-
if (url.indexOf('=JSONP_CALLBACK&') > -1) {
41-
url = StringWrapper.replace(url, '=JSONP_CALLBACK&', `=${callback}&`);
42-
} else if (url.lastIndexOf('=JSONP_CALLBACK') === url.length - '=JSONP_CALLBACK'.length) {
43-
url = StringWrapper.substring(url, 0, url.length - '=JSONP_CALLBACK'.length) + `=${callback}`;
44-
}
31+
this.response = new Observable(responseObserver => {
4532

46-
let script = this._script = _dom.build(url);
33+
this.readyState = ReadyStates.Loading;
34+
let id = this._id = _dom.nextRequestID();
4735

48-
script.addEventListener('load', (event) => {
49-
if (this.readyState === ReadyStates.Cancelled) return;
50-
this.readyState = ReadyStates.Done;
51-
_dom.cleanup(script);
52-
if (!this._finished) {
53-
ObservableWrapper.callThrow(
54-
this.response, makeTypeError('JSONP injected script did not invoke callback.'));
55-
return;
56-
}
36+
_dom.exposeConnection(id, this);
5737

58-
let responseOptions = new ResponseOptions({body: this._responseData});
59-
if (isPresent(this.baseResponseOptions)) {
60-
responseOptions = this.baseResponseOptions.merge(responseOptions);
38+
// Workaround Dart
39+
// url = url.replace(/=JSONP_CALLBACK(&|$)/, `generated method`);
40+
let callback = _dom.requestCallback(this._id);
41+
let url: string = req.url;
42+
if (url.indexOf('=JSONP_CALLBACK&') > -1) {
43+
url = StringWrapper.replace(url, '=JSONP_CALLBACK&', `=${callback}&`);
44+
} else if (url.lastIndexOf('=JSONP_CALLBACK') === url.length - '=JSONP_CALLBACK'.length) {
45+
url =
46+
StringWrapper.substring(url, 0, url.length - '=JSONP_CALLBACK'.length) + `=${callback}`;
6147
}
6248

63-
ObservableWrapper.callNext(this.response, new Response(responseOptions));
64-
});
49+
let script = this._script = _dom.build(url);
6550

66-
script.addEventListener('error', (error) => {
67-
if (this.readyState === ReadyStates.Cancelled) return;
68-
this.readyState = ReadyStates.Done;
69-
_dom.cleanup(script);
70-
ObservableWrapper.callThrow(this.response, error);
71-
});
51+
let onLoad = event => {
52+
if (this.readyState === ReadyStates.Cancelled) return;
53+
this.readyState = ReadyStates.Done;
54+
_dom.cleanup(script);
55+
if (!this._finished) {
56+
responseObserver.error(makeTypeError('JSONP injected script did not invoke callback.'));
57+
return;
58+
}
59+
60+
let responseOptions = new ResponseOptions({body: this._responseData});
61+
if (isPresent(this.baseResponseOptions)) {
62+
responseOptions = this.baseResponseOptions.merge(responseOptions);
63+
}
64+
65+
responseObserver.next(new Response(responseOptions));
66+
responseObserver.complete();
67+
};
68+
69+
let onError = error => {
70+
if (this.readyState === ReadyStates.Cancelled) return;
71+
this.readyState = ReadyStates.Done;
72+
_dom.cleanup(script);
73+
responseObserver.error(error);
74+
};
75+
76+
script.addEventListener('load', onLoad);
77+
script.addEventListener('error', onError);
7278

73-
_dom.send(script);
79+
_dom.send(script);
80+
81+
return () => {
82+
this.readyState = ReadyStates.Cancelled;
83+
script.removeEventListener('load', onLoad);
84+
script.removeEventListener('error', onError);
85+
if (isPresent(script)) {
86+
this._dom.cleanup(script);
87+
}
88+
89+
}
90+
});
7491
}
7592

7693
finished(data?: any) {
@@ -80,16 +97,6 @@ export class JSONPConnection implements Connection {
8097
if (this.readyState === ReadyStates.Cancelled) return;
8198
this._responseData = data;
8299
}
83-
84-
dispose(): void {
85-
this.readyState = ReadyStates.Cancelled;
86-
let script = this._script;
87-
this._script = null;
88-
if (isPresent(script)) {
89-
this._dom.cleanup(script);
90-
}
91-
ObservableWrapper.callReturn(this.response);
92-
}
93100
}
94101

95102
@Injectable()

modules/angular2/src/http/backends/xhr_backend.ts

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,78 +5,78 @@ import {Response} from '../static_response';
55
import {ResponseOptions, BaseResponseOptions} from '../base_response_options';
66
import {Injectable} from 'angular2/src/core/di';
77
import {BrowserXhr} from './browser_xhr';
8-
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
98
import {isPresent} from 'angular2/src/core/facade/lang';
10-
9+
var Observable = require('@reactivex/rxjs/dist/cjs/Observable');
1110
/**
12-
* Creates connections using `XMLHttpRequest`. Given a fully-qualified
13-
* request, an `XHRConnection` will immediately create an `XMLHttpRequest` object and send the
14-
* request.
15-
*
16-
* This class would typically not be created or interacted with directly inside applications, though
17-
* the {@link MockConnection} may be interacted with in tests.
18-
*/
11+
* Creates connections using `XMLHttpRequest`. Given a fully-qualified
12+
* request, an `XHRConnection` will immediately create an `XMLHttpRequest` object and send the
13+
* request.
14+
*
15+
* This class would typically not be created or interacted with directly inside applications, though
16+
* the {@link MockConnection} may be interacted with in tests.
17+
*/
1918
export class XHRConnection implements Connection {
2019
request: Request;
2120
/**
2221
* Response {@link EventEmitter} which emits a single {@link Response} value on load event of
2322
* `XMLHttpRequest`.
2423
*/
25-
response: EventEmitter; // TODO: Make generic of <Response>;
24+
response: any; // TODO: Make generic of <Response>;
2625
readyState: ReadyStates;
27-
private _xhr; // TODO: make type XMLHttpRequest, pending resolution of
28-
// https://github.com/angular/ts2dart/issues/230
2926
constructor(req: Request, browserXHR: BrowserXhr, baseResponseOptions?: ResponseOptions) {
3027
this.request = req;
31-
this.response = new EventEmitter();
32-
this._xhr = browserXHR.build();
33-
// TODO(jeffbcross): implement error listening/propagation
34-
this._xhr.open(RequestMethods[req.method].toUpperCase(), req.url);
35-
this._xhr.addEventListener('load', (_) => {
36-
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
37-
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
38-
let response = isPresent(this._xhr.response) ? this._xhr.response : this._xhr.responseText;
28+
this.response = new Observable(responseObserver => {
29+
let _xhr: XMLHttpRequest = browserXHR.build();
30+
_xhr.open(RequestMethods[req.method].toUpperCase(), req.url);
31+
// load event handler
32+
let onLoad = () => {
33+
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
34+
// response/responseType properties were introduced in XHR Level2 spec (supported by
35+
// IE10)
36+
let response = isPresent(_xhr.response) ? _xhr.response : _xhr.responseText;
3937

40-
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
41-
let status = this._xhr.status === 1223 ? 204 : this._xhr.status;
38+
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
39+
let status = _xhr.status === 1223 ? 204 : _xhr.status;
4240

43-
// fix status code when it is 0 (0 status is undocumented).
44-
// Occurs when accessing file resources or on Android 4.1 stock browser
45-
// while retrieving files from application cache.
46-
if (status === 0) {
47-
status = response ? 200 : 0;
48-
}
41+
// fix status code when it is 0 (0 status is undocumented).
42+
// Occurs when accessing file resources or on Android 4.1 stock browser
43+
// while retrieving files from application cache.
44+
if (status === 0) {
45+
status = response ? 200 : 0;
46+
}
47+
var responseOptions = new ResponseOptions({body: response, status: status});
48+
if (isPresent(baseResponseOptions)) {
49+
responseOptions = baseResponseOptions.merge(responseOptions);
50+
}
51+
responseObserver.next(new Response(responseOptions));
52+
// TODO(gdi2290): defer complete if array buffer until done
53+
responseObserver.complete();
54+
};
55+
// error event handler
56+
let onError = (err) => {
57+
var responseOptions = new ResponseOptions({body: err, type: ResponseTypes.Error});
58+
if (isPresent(baseResponseOptions)) {
59+
responseOptions = baseResponseOptions.merge(responseOptions);
60+
}
61+
responseObserver.error(new Response(responseOptions));
62+
};
4963

50-
var responseOptions = new ResponseOptions({body: response, status: status});
51-
if (isPresent(baseResponseOptions)) {
52-
responseOptions = baseResponseOptions.merge(responseOptions);
64+
if (isPresent(req.headers)) {
65+
req.headers.forEach((value, name) => { _xhr.setRequestHeader(name, value); });
5366
}
5467

55-
ObservableWrapper.callNext(this.response, new Response(responseOptions));
56-
// TODO(gdi2290): defer complete if array buffer until done
57-
ObservableWrapper.callReturn(this.response);
58-
});
68+
_xhr.addEventListener('load', onLoad);
69+
_xhr.addEventListener('error', onError);
5970

60-
this._xhr.addEventListener('error', (err) => {
61-
var responseOptions = new ResponseOptions({body: err, type: ResponseTypes.Error});
62-
if (isPresent(baseResponseOptions)) {
63-
responseOptions = baseResponseOptions.merge(responseOptions);
64-
}
65-
ObservableWrapper.callThrow(this.response, new Response(responseOptions));
66-
});
67-
// TODO(jeffbcross): make this more dynamic based on body type
71+
_xhr.send(this.request.text());
6872

69-
if (isPresent(req.headers)) {
70-
req.headers.forEach((value, name) => { this._xhr.setRequestHeader(name, value); });
71-
}
72-
73-
this._xhr.send(this.request.text());
73+
return () => {
74+
_xhr.removeEventListener('load', onLoad);
75+
_xhr.removeEventListener('error', onError);
76+
_xhr.abort();
77+
};
78+
});
7479
}
75-
76-
/**
77-
* Calls abort on the underlying XMLHttpRequest.
78-
*/
79-
dispose(): void { this._xhr.abort(); }
8080
}
8181

8282
/**

modules/angular2/src/http/http.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,9 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions {
3434
* Performs http requests using `XMLHttpRequest` as the default backend.
3535
*
3636
* `Http` is available as an injectable class, with methods to perform http requests. Calling
37-
* `request` returns an {@link EventEmitter} which will emit a single {@link Response} when a
37+
* `request` returns an {@link Observable} which will emit a single {@link Response} when a
3838
* response is received.
3939
*
40-
*
41-
* ## Breaking Change
42-
*
43-
* Previously, methods of `Http` would return an RxJS Observable directly. For now,
44-
* the `toRx()` method of {@link EventEmitter} needs to be called in order to get the RxJS
45-
* Subject. `EventEmitter` does not provide combinators like `map`, and has different semantics for
46-
* subscribing/observing. This is temporary; the result of all `Http` method calls will be either an
47-
* Observable
48-
* or Dart Stream when [issue #2794](https://github.com/angular/angular/issues/2794) is resolved.
49-
*
5040
* #Example
5141
*
5242
* ```
@@ -56,8 +46,6 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions {
5646
* class PeopleComponent {
5747
* constructor(http: Http) {
5848
* http.get('people.json')
59-
* //Get the RxJS Subject
60-
* .toRx()
6149
* // Call map on the response observable to get the parsed people object
6250
* .map(res => res.json())
6351
* // Subscribe to the observable to get the parsed people object and attach it to the
@@ -67,9 +55,6 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions {
6755
* }
6856
* ```
6957
*
70-
* To use the {@link EventEmitter} returned by `Http`, simply pass a generator (See "interface
71-
*Generator" in the Async Generator spec: https://github.com/jhusain/asyncgenerator) to the
72-
*`observer` method of the returned emitter, with optional methods of `next`, `throw`, and `return`.
7358
*
7459
* #Example
7560
*
@@ -95,7 +80,7 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions {
9580
* [MockBackend, BaseRequestOptions])
9681
* ]);
9782
* var http = injector.get(Http);
98-
* http.get('request-from-mock-backend.json').toRx().subscribe((res:Response) => doSomething(res));
83+
* http.get('request-from-mock-backend.json').subscribe((res:Response) => doSomething(res));
9984
* ```
10085
*
10186
**/

modules/angular2/src/http/interfaces.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export class Connection {
2626
readyState: ReadyStates;
2727
request: Request;
2828
response: EventEmitter; // TODO: generic of <Response>;
29-
dispose(): void { throw new BaseException('Abstract!'); }
3029
}
3130

3231
/**

0 commit comments

Comments
 (0)