refactor(http): move http files to top-level module
Closes #2680 Closes #3417
This commit is contained in:
@ -1,62 +0,0 @@
|
||||
library angular2.src.http.backends.browser_jsonp;
|
||||
|
||||
import 'package:angular2/di.dart';
|
||||
import 'dart:html' show document;
|
||||
import 'dart:js' show context, JsObject, JsArray;
|
||||
|
||||
int _nextRequestId = 0;
|
||||
const JSONP_HOME = '__ng_jsonp__';
|
||||
|
||||
var _jsonpConnections = null;
|
||||
|
||||
JsObject _getJsonpConnections() {
|
||||
if (_jsonpConnections == null) {
|
||||
_jsonpConnections = context[JSONP_HOME] = new JsObject(context['Object']);
|
||||
}
|
||||
return _jsonpConnections;
|
||||
}
|
||||
|
||||
// Make sure not to evaluate this in a non-browser environment!
|
||||
@Injectable()
|
||||
class BrowserJsonp {
|
||||
// Construct a <script> element with the specified URL
|
||||
dynamic build(String url) {
|
||||
var node = document.createElement('script');
|
||||
node.src = url;
|
||||
return node;
|
||||
}
|
||||
|
||||
nextRequestID() {
|
||||
return "__req${_nextRequestId++}";
|
||||
}
|
||||
|
||||
requestCallback(String id) {
|
||||
return """${JSONP_HOME}.${id}.finished""";
|
||||
}
|
||||
|
||||
exposeConnection(String id, dynamic connection) {
|
||||
var connections = _getJsonpConnections();
|
||||
var wrapper = new JsObject(context['Object']);
|
||||
|
||||
wrapper['_id'] = id;
|
||||
wrapper['__dart__'] = connection;
|
||||
wrapper['finished'] = ([dynamic data]) => connection.finished(data);
|
||||
|
||||
connections[id] = wrapper;
|
||||
}
|
||||
|
||||
removeConnection(String id) {
|
||||
var connections = _getJsonpConnections();
|
||||
connections[id] = null;
|
||||
}
|
||||
|
||||
// Attach the <script> element to the DOM
|
||||
send(dynamic node) {
|
||||
document.body.append(node);
|
||||
}
|
||||
|
||||
// Remove <script> element from the DOM
|
||||
cleanup(dynamic node) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {global} from 'angular2/src/facade/lang';
|
||||
|
||||
let _nextRequestId = 0;
|
||||
export const JSONP_HOME = '__ng_jsonp__';
|
||||
var _jsonpConnections = null;
|
||||
|
||||
function _getJsonpConnections(): {[key: string]: any} {
|
||||
if (_jsonpConnections === null) {
|
||||
_jsonpConnections = global[JSONP_HOME] = {};
|
||||
}
|
||||
return _jsonpConnections;
|
||||
}
|
||||
|
||||
// Make sure not to evaluate this in a non-browser environment!
|
||||
@Injectable()
|
||||
export class BrowserJsonp {
|
||||
// Construct a <script> element with the specified URL
|
||||
build(url: string): any {
|
||||
let node = document.createElement('script');
|
||||
node.src = url;
|
||||
return node;
|
||||
}
|
||||
|
||||
nextRequestID(): string { return `__req${_nextRequestId++}`; }
|
||||
|
||||
requestCallback(id: string): string { return `${JSONP_HOME}.${id}.finished`; }
|
||||
|
||||
exposeConnection(id: string, connection: any) {
|
||||
let connections = _getJsonpConnections();
|
||||
connections[id] = connection;
|
||||
}
|
||||
|
||||
removeConnection(id: string) {
|
||||
var connections = _getJsonpConnections();
|
||||
connections[id] = null;
|
||||
}
|
||||
|
||||
// Attach the <script> element to the DOM
|
||||
send(node: any) { document.body.appendChild(<Node>(node)); }
|
||||
|
||||
// Remove <script> element from the DOM
|
||||
cleanup(node: any) {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(<Node>(node));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
library angular2.src.http.backends.browser_xhr;
|
||||
|
||||
import 'dart:html' show HttpRequest;
|
||||
import 'package:angular2/di.dart';
|
||||
|
||||
@Injectable()
|
||||
class BrowserXhr {
|
||||
HttpRequest build() {
|
||||
return new HttpRequest();
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
|
||||
// Make sure not to evaluate this in a non-browser environment!
|
||||
@Injectable()
|
||||
export class BrowserXhr {
|
||||
constructor() {}
|
||||
build(): any { return <any>(new XMLHttpRequest()); }
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
import {ConnectionBackend, Connection} from '../interfaces';
|
||||
import {ReadyStates, RequestMethods, RequestMethodsMap} from '../enums';
|
||||
import {Request} from '../static_request';
|
||||
import {Response} from '../static_response';
|
||||
import {ResponseOptions, BaseResponseOptions} from '../base_response_options';
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {BrowserJsonp} from './browser_jsonp';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {StringWrapper, isPresent, ENUM_INDEX, makeTypeError} from 'angular2/src/facade/lang';
|
||||
|
||||
export class JSONPConnection implements Connection {
|
||||
readyState: ReadyStates;
|
||||
request: Request;
|
||||
response: EventEmitter;
|
||||
private _id: string;
|
||||
private _script: Element;
|
||||
private _responseData: any;
|
||||
private _finished: boolean = false;
|
||||
|
||||
constructor(req: Request, private _dom: BrowserJsonp,
|
||||
private baseResponseOptions?: ResponseOptions) {
|
||||
if (req.method !== RequestMethods.GET) {
|
||||
throw makeTypeError("JSONP requests must use GET request method.");
|
||||
}
|
||||
this.request = req;
|
||||
this.response = new EventEmitter();
|
||||
this.readyState = ReadyStates.LOADING;
|
||||
this._id = _dom.nextRequestID();
|
||||
|
||||
_dom.exposeConnection(this._id, this);
|
||||
|
||||
// Workaround Dart
|
||||
// url = url.replace(/=JSONP_CALLBACK(&|$)/, `generated method`);
|
||||
let callback = _dom.requestCallback(this._id);
|
||||
let url: string = req.url;
|
||||
if (url.indexOf('=JSONP_CALLBACK&') > -1) {
|
||||
url = StringWrapper.replace(url, '=JSONP_CALLBACK&', `=${callback}&`);
|
||||
} else if (url.lastIndexOf('=JSONP_CALLBACK') === url.length - '=JSONP_CALLBACK'.length) {
|
||||
url = StringWrapper.substring(url, 0, url.length - '=JSONP_CALLBACK'.length) + `=${callback}`;
|
||||
}
|
||||
|
||||
let script = this._script = _dom.build(url);
|
||||
|
||||
script.addEventListener('load', (event) => {
|
||||
if (this.readyState === ReadyStates.CANCELLED) return;
|
||||
this.readyState = ReadyStates.DONE;
|
||||
_dom.cleanup(script);
|
||||
if (!this._finished) {
|
||||
ObservableWrapper.callThrow(
|
||||
this.response, makeTypeError('JSONP injected script did not invoke callback.'));
|
||||
return;
|
||||
}
|
||||
|
||||
let responseOptions = new ResponseOptions({body: this._responseData});
|
||||
if (isPresent(this.baseResponseOptions)) {
|
||||
responseOptions = this.baseResponseOptions.merge(responseOptions);
|
||||
}
|
||||
|
||||
ObservableWrapper.callNext(this.response, new Response(responseOptions));
|
||||
});
|
||||
|
||||
script.addEventListener('error', (error) => {
|
||||
if (this.readyState === ReadyStates.CANCELLED) return;
|
||||
this.readyState = ReadyStates.DONE;
|
||||
_dom.cleanup(script);
|
||||
ObservableWrapper.callThrow(this.response, error);
|
||||
});
|
||||
|
||||
_dom.send(script);
|
||||
}
|
||||
|
||||
finished(data?: any) {
|
||||
// Don't leak connections
|
||||
this._finished = true;
|
||||
this._dom.removeConnection(this._id);
|
||||
if (this.readyState === ReadyStates.CANCELLED) return;
|
||||
this._responseData = data;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.readyState = ReadyStates.CANCELLED;
|
||||
let script = this._script;
|
||||
this._script = null;
|
||||
if (isPresent(script)) {
|
||||
this._dom.cleanup(script);
|
||||
}
|
||||
ObservableWrapper.callReturn(this.response);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JSONPBackend implements ConnectionBackend {
|
||||
constructor(private _browserJSONP: BrowserJsonp, private _baseResponseOptions: ResponseOptions) {}
|
||||
createConnection(request: Request): JSONPConnection {
|
||||
return new JSONPConnection(request, this._browserJSONP, this._baseResponseOptions);
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {Request} from 'angular2/src/http/static_request';
|
||||
import {Response} from 'angular2/src/http/static_response';
|
||||
import {ReadyStates} from 'angular2/src/http/enums';
|
||||
import {Connection, ConnectionBackend} from 'angular2/src/http/interfaces';
|
||||
import {ObservableWrapper, EventEmitter} from 'angular2/src/facade/async';
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
import {IMPLEMENTS, BaseException} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
*
|
||||
* Mock Connection to represent a {@link Connection} for tests.
|
||||
*
|
||||
**/
|
||||
@IMPLEMENTS(Connection)
|
||||
export class MockConnection {
|
||||
// TODO Name `readyState` should change to be more generic, and states could be made to be more
|
||||
// descriptive than XHR states.
|
||||
/**
|
||||
* Describes the state of the connection, based on `XMLHttpRequest.readyState`, but with
|
||||
* additional states. For example, state 5 indicates an aborted connection.
|
||||
*/
|
||||
readyState: ReadyStates;
|
||||
|
||||
/**
|
||||
* {@link Request} instance used to create the connection.
|
||||
*/
|
||||
request: Request;
|
||||
|
||||
/**
|
||||
* {@link EventEmitter} of {@link Response}. Can be subscribed to in order to be notified when a
|
||||
* response is available.
|
||||
*/
|
||||
response: EventEmitter;
|
||||
|
||||
constructor(req: Request) {
|
||||
this.response = new EventEmitter();
|
||||
this.readyState = ReadyStates.OPEN;
|
||||
this.request = req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the `readyState` of the connection to a custom state of 5 (cancelled).
|
||||
*/
|
||||
dispose() {
|
||||
if (this.readyState !== ReadyStates.DONE) {
|
||||
this.readyState = ReadyStates.CANCELLED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a mock response to the connection. This response is the value that is emitted to the
|
||||
* {@link EventEmitter} returned by {@link Http}.
|
||||
*
|
||||
* #Example
|
||||
*
|
||||
* ```
|
||||
* var connection;
|
||||
* backend.connections.subscribe(c => connection = c);
|
||||
* http.request('data.json').subscribe(res => console.log(res.text()));
|
||||
* connection.mockRespond(new Response('fake response')); //logs 'fake response'
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
mockRespond(res: Response) {
|
||||
if (this.readyState === ReadyStates.DONE || this.readyState === ReadyStates.CANCELLED) {
|
||||
throw new BaseException('Connection has already been resolved');
|
||||
}
|
||||
this.readyState = ReadyStates.DONE;
|
||||
ObservableWrapper.callNext(this.response, res);
|
||||
ObservableWrapper.callReturn(this.response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented!
|
||||
*
|
||||
* Sends the provided {@link Response} to the `downloadObserver` of the `Request`
|
||||
* associated with this connection.
|
||||
*/
|
||||
mockDownload(res: Response) {
|
||||
// this.request.downloadObserver.onNext(res);
|
||||
// if (res.bytesLoaded === res.totalBytes) {
|
||||
// this.request.downloadObserver.onCompleted();
|
||||
// }
|
||||
}
|
||||
|
||||
// TODO(jeffbcross): consider using Response type
|
||||
/**
|
||||
* Emits the provided error object as an error to the {@link Response} {@link EventEmitter}
|
||||
* returned
|
||||
* from {@link Http}.
|
||||
*/
|
||||
mockError(err?: Error) {
|
||||
// Matches XHR semantics
|
||||
this.readyState = ReadyStates.DONE;
|
||||
ObservableWrapper.callThrow(this.response, err);
|
||||
ObservableWrapper.callReturn(this.response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock backend for testing the {@link Http} service.
|
||||
*
|
||||
* This class can be injected in tests, and should be used to override bindings
|
||||
* to other backends, such as {@link XHRBackend}.
|
||||
*
|
||||
* #Example
|
||||
*
|
||||
* ```
|
||||
* import {MockBackend, DefaultOptions, Http} from 'angular2/http';
|
||||
* it('should get some data', inject([AsyncTestCompleter], (async) => {
|
||||
* var connection;
|
||||
* var injector = Injector.resolveAndCreate([
|
||||
* MockBackend,
|
||||
* bind(Http).toFactory((backend, defaultOptions) => {
|
||||
* return new Http(backend, defaultOptions)
|
||||
* }, [MockBackend, DefaultOptions])]);
|
||||
* var http = injector.get(Http);
|
||||
* var backend = injector.get(MockBackend);
|
||||
* //Assign any newly-created connection to local variable
|
||||
* backend.connections.subscribe(c => connection = c);
|
||||
* http.request('data.json').subscribe((res) => {
|
||||
* expect(res.text()).toBe('awesome');
|
||||
* async.done();
|
||||
* });
|
||||
* connection.mockRespond(new Response('awesome'));
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* This method only exists in the mock implementation, not in real Backends.
|
||||
**/
|
||||
@Injectable()
|
||||
@IMPLEMENTS(ConnectionBackend)
|
||||
export class MockBackend {
|
||||
/**
|
||||
* {@link EventEmitter}
|
||||
* of {@link MockConnection} instances that have been created by this backend. Can be subscribed
|
||||
* to in order to respond to connections.
|
||||
*
|
||||
* #Example
|
||||
*
|
||||
* ```
|
||||
* import {MockBackend, Http, BaseRequestOptions} from 'angular2/http';
|
||||
* import {Injector} from 'angular2/di';
|
||||
*
|
||||
* it('should get a response', () => {
|
||||
* var connection; //this will be set when a new connection is emitted from the backend.
|
||||
* var text; //this will be set from mock response
|
||||
* var injector = Injector.resolveAndCreate([
|
||||
* MockBackend,
|
||||
* bind(Http).toFactory(backend, options) {
|
||||
* return new Http(backend, options);
|
||||
* }, [MockBackend, BaseRequestOptions]]);
|
||||
* var backend = injector.get(MockBackend);
|
||||
* var http = injector.get(Http);
|
||||
* backend.connections.subscribe(c => connection = c);
|
||||
* http.request('something.json').subscribe(res => {
|
||||
* text = res.text();
|
||||
* });
|
||||
* connection.mockRespond(new Response({body: 'Something'}));
|
||||
* expect(text).toBe('Something');
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This property only exists in the mock implementation, not in real Backends.
|
||||
*/
|
||||
connections: EventEmitter; //<MockConnection>
|
||||
|
||||
/**
|
||||
* An array representation of `connections`. This array will be updated with each connection that
|
||||
* is created by this backend.
|
||||
*
|
||||
* This property only exists in the mock implementation, not in real Backends.
|
||||
*/
|
||||
connectionsArray: Array<MockConnection>;
|
||||
/**
|
||||
* {@link EventEmitter} of {@link MockConnection} instances that haven't yet been resolved (i.e.
|
||||
* with a `readyState`
|
||||
* less than 4). Used internally to verify that no connections are pending via the
|
||||
* `verifyNoPendingRequests` method.
|
||||
*
|
||||
* This property only exists in the mock implementation, not in real Backends.
|
||||
*/
|
||||
pendingConnections: EventEmitter; //<MockConnection>
|
||||
constructor() {
|
||||
this.connectionsArray = [];
|
||||
this.connections = new EventEmitter();
|
||||
ObservableWrapper.subscribe<MockConnection>(
|
||||
this.connections, connection => this.connectionsArray.push(connection));
|
||||
this.pendingConnections = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks all connections, and raises an exception if any connection has not received a response.
|
||||
*
|
||||
* This method only exists in the mock implementation, not in real Backends.
|
||||
*/
|
||||
verifyNoPendingRequests() {
|
||||
let pending = 0;
|
||||
ObservableWrapper.subscribe(this.pendingConnections, c => pending++);
|
||||
if (pending > 0) throw new BaseException(`${pending} pending connections to be resolved`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used in conjunction with `verifyNoPendingRequests` to resolve any not-yet-resolve
|
||||
* connections, if it's expected that there are connections that have not yet received a response.
|
||||
*
|
||||
* This method only exists in the mock implementation, not in real Backends.
|
||||
*/
|
||||
resolveAllConnections() {
|
||||
ObservableWrapper.subscribe<MockConnection>(this.connections, c => c.readyState = 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MockConnection}. This is equivalent to calling `new
|
||||
* MockConnection()`, except that it also will emit the new `Connection` to the `connections`
|
||||
* emitter of this `MockBackend` instance. This method will usually only be used by tests
|
||||
* against the framework itself, not by end-users.
|
||||
*/
|
||||
createConnection(req: Request): Connection {
|
||||
if (!isPresent(req) || !(req instanceof Request)) {
|
||||
throw new BaseException(`createConnection requires an instance of Request, got ${req}`);
|
||||
}
|
||||
let connection = new MockConnection(req);
|
||||
ObservableWrapper.callNext(this.connections, connection);
|
||||
return connection;
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
import {ConnectionBackend, Connection} from '../interfaces';
|
||||
import {ReadyStates, RequestMethods, RequestMethodsMap} from '../enums';
|
||||
import {Request} from '../static_request';
|
||||
import {Response} from '../static_response';
|
||||
import {ResponseOptions, BaseResponseOptions} from '../base_response_options';
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {BrowserXhr} from './browser_xhr';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||
import {isPresent, ENUM_INDEX} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* Creates connections using `XMLHttpRequest`. Given a fully-qualified
|
||||
* request, an `XHRConnection` will immediately create an `XMLHttpRequest` object and send the
|
||||
* request.
|
||||
*
|
||||
* This class would typically not be created or interacted with directly inside applications, though
|
||||
* the {@link MockConnection} may be interacted with in tests.
|
||||
*/
|
||||
export class XHRConnection implements Connection {
|
||||
request: Request;
|
||||
/**
|
||||
* Response {@link EventEmitter} which emits a single {@link Response} value on load event of
|
||||
* `XMLHttpRequest`.
|
||||
*/
|
||||
response: EventEmitter; // TODO: Make generic of <Response>;
|
||||
readyState: ReadyStates;
|
||||
private _xhr; // TODO: make type XMLHttpRequest, pending resolution of
|
||||
// https://github.com/angular/ts2dart/issues/230
|
||||
constructor(req: Request, browserXHR: BrowserXhr, baseResponseOptions?: ResponseOptions) {
|
||||
// TODO: get rid of this when enum lookups are available in ts2dart
|
||||
// https://github.com/angular/ts2dart/issues/221
|
||||
var requestMethodsMap = new RequestMethodsMap();
|
||||
this.request = req;
|
||||
this.response = new EventEmitter();
|
||||
this._xhr = browserXHR.build();
|
||||
// TODO(jeffbcross): implement error listening/propagation
|
||||
this._xhr.open(requestMethodsMap.getMethod(ENUM_INDEX(req.method)), req.url);
|
||||
this._xhr.addEventListener('load', (_) => {
|
||||
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
|
||||
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
|
||||
let response = isPresent(this._xhr.response) ? this._xhr.response : this._xhr.responseText;
|
||||
|
||||
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
|
||||
let status = this._xhr.status === 1223 ? 204 : this._xhr.status;
|
||||
|
||||
// fix status code when it is 0 (0 status is undocumented).
|
||||
// Occurs when accessing file resources or on Android 4.1 stock browser
|
||||
// while retrieving files from application cache.
|
||||
if (status === 0) {
|
||||
status = response ? 200 : 0;
|
||||
}
|
||||
|
||||
var responseOptions = new ResponseOptions({body: response, status: status});
|
||||
if (isPresent(baseResponseOptions)) {
|
||||
responseOptions = baseResponseOptions.merge(responseOptions);
|
||||
}
|
||||
|
||||
ObservableWrapper.callNext(this.response, new Response(responseOptions));
|
||||
// TODO(gdi2290): defer complete if array buffer until done
|
||||
ObservableWrapper.callReturn(this.response);
|
||||
});
|
||||
// TODO(jeffbcross): make this more dynamic based on body type
|
||||
|
||||
if (isPresent(req.headers)) {
|
||||
req.headers.forEach((value, name) => { this._xhr.setRequestHeader(name, value); });
|
||||
}
|
||||
|
||||
this._xhr.send(this.request.text());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls abort on the underlying XMLHttpRequest.
|
||||
*/
|
||||
dispose(): void { this._xhr.abort(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link XHRConnection} instances.
|
||||
*
|
||||
* This class would typically not be used by end users, but could be
|
||||
* overridden if a different backend implementation should be used,
|
||||
* such as in a node backend.
|
||||
*
|
||||
* #Example
|
||||
*
|
||||
* ```
|
||||
* import {Http, MyNodeBackend, httpInjectables, BaseRequestOptions} from 'angular2/http';
|
||||
* @Component({
|
||||
* viewBindings: [
|
||||
* httpInjectables,
|
||||
* bind(Http).toFactory((backend, options) => {
|
||||
* return new Http(backend, options);
|
||||
* }, [MyNodeBackend, BaseRequestOptions])]
|
||||
* })
|
||||
* class MyComponent {
|
||||
* constructor(http:Http) {
|
||||
* http('people.json').subscribe(res => this.people = res.json());
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
**/
|
||||
@Injectable()
|
||||
export class XHRBackend implements ConnectionBackend {
|
||||
constructor(private _browserXHR: BrowserXhr, private _baseResponseOptions: ResponseOptions) {}
|
||||
createConnection(request: Request): XHRConnection {
|
||||
return new XHRConnection(request, this._browserXHR, this._baseResponseOptions);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user