diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js
index cd73e6d06..d3ab881ac 100644
--- a/Examples/UIExplorer/UIExplorerList.js
+++ b/Examples/UIExplorer/UIExplorerList.js
@@ -77,6 +77,7 @@ var APIS = [
require('./StatusBarIOSExample'),
require('./TimerExample'),
require('./VibrationIOSExample'),
+ require('./XHRExample'),
];
var ds = new ListView.DataSource({
diff --git a/Examples/UIExplorer/XHRExample.js b/Examples/UIExplorer/XHRExample.js
new file mode 100644
index 000000000..c5b350c70
--- /dev/null
+++ b/Examples/UIExplorer/XHRExample.js
@@ -0,0 +1,131 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ ProgressViewIOS,
+ StyleSheet,
+ View,
+ Text,
+ TouchableHighlight,
+} = React;
+
+class Downloader extends React.Component {
+
+ xhr: XMLHttpRequest;
+ cancelled: boolean;
+
+ constructor(props) {
+ super(props);
+ this.cancelled = false;
+ this.state = {
+ downloading: false,
+ contentSize: 1,
+ downloaded: 0,
+ };
+ }
+
+ download() {
+ this.xhr && this.xhr.abort();
+
+ var xhr = this.xhr || new XMLHttpRequest();
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === xhr.HEADERS_RECEIVED) {
+ var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10);
+ this.setState({
+ contentSize: contentSize,
+ downloaded: 0,
+ });
+ } else if (xhr.readyState === xhr.LOADING) {
+ this.setState({
+ downloaded: xhr.responseText.length,
+ });
+ } else if (xhr.readyState === xhr.DONE) {
+ this.setState({
+ downloading: false,
+ });
+ if (this.cancelled) {
+ this.cancelled = false;
+ return;
+ }
+ if (xhr.status === 200) {
+ alert('Download complete!');
+ } else if (xhr.status !== 0) {
+ alert('Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText);
+ } else {
+ alert('Error: ' + xhr.responseText);
+ }
+ }
+ };
+ xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt');
+ xhr.send();
+ this.xhr = xhr;
+
+ this.setState({downloading: true});
+ }
+
+ componentWillUnmount() {
+ this.cancelled = true;
+ this.xhr && this.xhr.abort();
+ }
+
+ render() {
+ var button = this.state.downloading ? (
+
+
+ Downloading...
+
+
+ ) : (
+
+
+ Download 5MB Text File
+
+
+ );
+
+ return (
+
+ {button}
+
+
+ );
+ }
+}
+
+exports.framework = 'React';
+exports.title = 'XMLHttpRequest';
+exports.description = 'XMLHttpRequest';
+exports.examples = [{
+ title: 'File Download',
+ render() {
+ return ;
+ }
+}];
+
+var styles = StyleSheet.create({
+ wrapper: {
+ borderRadius: 5,
+ marginBottom: 5,
+ },
+ button: {
+ backgroundColor: '#eeeeee',
+ padding: 10,
+ },
+});
diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
index 81978ee0c..f9fe1523d 100644
--- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
+++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
@@ -85,7 +85,7 @@ function setUpAlert() {
var alertOpts = {
title: 'Alert',
message: '' + text,
- buttons: [{'cancel': 'Okay'}],
+ buttons: [{'cancel': 'OK'}],
};
RCTAlertManager.alertWithArgs(alertOpts, null);
};
diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m
index f4497a187..35400955b 100644
--- a/Libraries/Network/RCTDataManager.m
+++ b/Libraries/Network/RCTDataManager.m
@@ -11,10 +11,21 @@
#import "RCTAssert.h"
#import "RCTConvert.h"
+#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTUtils.h"
+@interface RCTDataManager ()
+
+@end
+
@implementation RCTDataManager
+{
+ NSURLSession *_session;
+ NSOperationQueue *_callbackQueue;
+}
+
+@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
@@ -24,6 +35,7 @@ RCT_EXPORT_MODULE()
*/
RCT_EXPORT_METHOD(queryData:(NSString *)queryType
withQuery:(NSDictionary *)query
+ sendIncrementalUpdates:(BOOL)incrementalUpdates
responseSender:(RCTResponseSenderBlock)responseSender)
{
if ([queryType isEqualToString:@"http"]) {
@@ -35,41 +47,30 @@ RCT_EXPORT_METHOD(queryData:(NSString *)queryType
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
request.HTTPBody = [RCTConvert NSData:query[@"data"]];
- // Build data task
- NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *connectionError) {
+ // Create session if one doesn't already exist
+ if (!_session) {
+ _callbackQueue = [[NSOperationQueue alloc] init];
+ NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
+ _session = [NSURLSession sessionWithConfiguration:configuration
+ delegate:self
+ delegateQueue:_callbackQueue];
+ }
- NSHTTPURLResponse *httpResponse = nil;
- if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
- // Might be a local file request
- httpResponse = (NSHTTPURLResponse *)response;
- }
-
- // Build response
- NSArray *responseJSON;
- if (connectionError == nil) {
- NSStringEncoding encoding = NSUTF8StringEncoding;
- if (response.textEncodingName) {
- CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
- encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
+ __block NSURLSessionDataTask *task;
+ if (incrementalUpdates) {
+ task = [_session dataTaskWithRequest:request];
+ } else {
+ task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+ RCTSendResponseEvent(_bridge, task);
+ if (!error) {
+ RCTSendDataEvent(_bridge, task, data);
}
- responseJSON = @[
- @(httpResponse.statusCode ?: 200),
- httpResponse.allHeaderFields ?: @{},
- [[NSString alloc] initWithData:data encoding:encoding] ?: @"",
- ];
- } else {
- responseJSON = @[
- @(httpResponse.statusCode),
- httpResponse.allHeaderFields ?: @{},
- connectionError.localizedDescription ?: [NSNull null],
- ];
- }
-
- // Send response (won't be sent on same thread as caller)
- responseSender(responseJSON);
-
- }];
+ RCTSendCompletionEvent(_bridge, task, error);
+ }];
+ }
+ // Build data task
+ responseSender(@[@(task.taskIdentifier)]);
[task resume];
} else {
@@ -78,4 +79,78 @@ RCT_EXPORT_METHOD(queryData:(NSString *)queryType
}
}
+#pragma mark - URLSession delegate
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)task
+didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
+{
+ RCTSendResponseEvent(_bridge, task);
+ completionHandler(NSURLSessionResponseAllow);
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)task
+ didReceiveData:(NSData *)data
+{
+ RCTSendDataEvent(_bridge, task, data);
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
+{
+ RCTSendCompletionEvent(_bridge, task, error);
+}
+
+#pragma mark - Build responses
+
+static void RCTSendResponseEvent(RCTBridge *bridge, NSURLSessionTask *task)
+{
+ NSURLResponse *response = task.response;
+ NSHTTPURLResponse *httpResponse = nil;
+ if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+ // Might be a local file request
+ httpResponse = (NSHTTPURLResponse *)response;
+ }
+
+ NSArray *responseJSON = @[@(task.taskIdentifier),
+ @(httpResponse.statusCode ?: 200),
+ httpResponse.allHeaderFields ?: @{},
+ ];
+
+ [bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse"
+ body:responseJSON];
+}
+
+static void RCTSendDataEvent(RCTBridge *bridge, NSURLSessionDataTask *task, NSData *data)
+{
+ // Get text encoding
+ NSURLResponse *response = task.response;
+ NSStringEncoding encoding = NSUTF8StringEncoding;
+ if (response.textEncodingName) {
+ CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
+ encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
+ }
+
+ NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding];
+ if (!responseText && data.length) {
+ RCTLogError(@"Received data was invalid.");
+ return;
+ }
+
+ NSArray *responseJSON = @[@(task.taskIdentifier), responseText ?: @""];
+ [bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData"
+ body:responseJSON];
+}
+
+static void RCTSendCompletionEvent(RCTBridge *bridge, NSURLSessionTask *task, NSError *error)
+{
+ NSArray *responseJSON = @[@(task.taskIdentifier),
+ error.localizedDescription ?: [NSNull null],
+ ];
+
+ [bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
+ body:responseJSON];
+}
+
@end
diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js
index 9249047da..a54822dab 100644
--- a/Libraries/Network/XMLHttpRequest.ios.js
+++ b/Libraries/Network/XMLHttpRequest.ios.js
@@ -12,11 +12,73 @@
'use strict';
var RCTDataManager = require('NativeModules').DataManager;
+var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
var XMLHttpRequestBase = require('XMLHttpRequestBase');
class XMLHttpRequest extends XMLHttpRequestBase {
+ _requestId: ?number;
+ _subscriptions: [any];
+
+ constructor() {
+ super();
+ this._requestId = null;
+ this._subscriptions = [];
+ }
+
+ _didCreateRequest(requestId: number): void {
+ this._requestId = requestId;
+ this._subscriptions.push(RCTDeviceEventEmitter.addListener(
+ 'didReceiveNetworkResponse',
+ (args) => this._didReceiveResponse.call(this, args[0], args[1], args[2])
+ ));
+ this._subscriptions.push(RCTDeviceEventEmitter.addListener(
+ 'didReceiveNetworkData',
+ (args) => this._didReceiveData.call(this, args[0], args[1])
+ ));
+ this._subscriptions.push(RCTDeviceEventEmitter.addListener(
+ 'didCompleteNetworkResponse',
+ (args) => this._didCompleteResponse.apply(this, args[0], args[1])
+ ));
+ }
+
+ _didReceiveResponse(requestId: number, status: number, responseHeaders: ?Object): void {
+ if (requestId === this._requestId) {
+ this.status = status;
+ this.setResponseHeaders(responseHeaders);
+ this.setReadyState(this.HEADERS_RECEIVED);
+ }
+ }
+
+ _didReceiveData(requestId: number, responseText: string): void {
+ if (requestId === this._requestId) {
+ if (!this.responseText) {
+ this.responseText = responseText;
+ } else {
+ this.responseText += responseText;
+ }
+ this.setReadyState(this.LOADING);
+ }
+ }
+
+ _didCompleteResponse(requestId: number, error: string): void {
+ if (requestId === this._requestId) {
+ this.responseText = error;
+ this._clearSubscriptions();
+ this._requestId = null;
+ this.setReadyState(this.DONE);
+ }
+ }
+
+ _clearSubscriptions(): void {
+ for (var i = 0; i < this._subscriptions.length; i++) {
+ var sub = this._subscriptions[i];
+ sub.remove();
+ }
+ this._subscriptions = [];
+ }
+
sendImpl(method: ?string, url: ?string, headers: Object, data: any): void {
RCTDataManager.queryData(
'http',
@@ -26,11 +88,16 @@ class XMLHttpRequest extends XMLHttpRequestBase {
data: data,
headers: headers,
},
- this.callback.bind(this)
+ this.onreadystatechange ? true : false,
+ this._didCreateRequest.bind(this)
);
}
abortImpl(): void {
+ if (this._requestId) {
+ this._clearSubscriptions();
+ this._requestId = null;
+ }
console.warn(
'XMLHttpRequest: abort() cancels JS callbacks ' +
'but not native HTTP request.'
diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequestBase.js
index 3570e4bf2..9d06f486a 100644
--- a/Libraries/Network/XMLHttpRequestBase.js
+++ b/Libraries/Network/XMLHttpRequestBase.js
@@ -1,8 +1,13 @@
/**
- * Copyright 2004-present Facebook. All Rights Reserved.
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
*
- * @flow
* @providesModule XMLHttpRequestBase
+ * @flow
*/
'use strict';
@@ -30,6 +35,7 @@ class XMLHttpRequestBase {
_headers: Object;
_sent: boolean;
_aborted: boolean;
+ _lowerCaseResponseHeaders: Object;
constructor() {
this.UNSENT = 0;
@@ -38,46 +44,52 @@ class XMLHttpRequestBase {
this.LOADING = 3;
this.DONE = 4;
- this.onreadystatechange = undefined;
- this.upload = undefined; /* Upload not supported */
- this.readyState = this.UNSENT;
- this.responseHeaders = undefined;
- this.responseText = undefined;
- this.status = 0;
+ this.onreadystatechange = null;
+ this.onload = null;
+ this.upload = undefined; /* Upload not supported yet */
+ this._reset();
this._method = null;
this._url = null;
- this._headers = {};
- this._sent = false;
this._aborted = false;
}
+ _reset() {
+ this.readyState = this.UNSENT;
+ this.responseHeaders = undefined;
+ this.responseText = '';
+ this.status = 0;
+
+ this._headers = {};
+ this._sent = false;
+ this._lowerCaseResponseHeaders = {};
+ }
+
getAllResponseHeaders(): ?string {
- if (this.responseHeaders) {
- var headers = [];
- for (var headerName in this.responseHeaders) {
- headers.push(headerName + ': ' + this.responseHeaders[headerName]);
- }
- return headers.join('\n');
+ if (!this.responseHeaders) {
+ // according to the spec, return null if no response has been received
+ return null;
}
- // according to the spec, return null <==> no response has been received
- return null;
+ var headers = this.responseHeaders || {};
+ return Object.keys(headers).map((headerName) => {
+ return headerName + ': ' + headers[headerName];
+ }).join('\n');
}
getResponseHeader(header: string): ?string {
- if (this.responseHeaders) {
- var value = this.responseHeaders[header.toLowerCase()];
- return value !== undefined ? value : null;
- }
- return null;
+ var value = this._lowerCaseResponseHeaders[header.toLowerCase()];
+ return value !== undefined ? value : null;
}
setRequestHeader(header: string, value: any): void {
+ if (this.readyState !== this.OPENED) {
+ throw new Error('Request has not been opened');
+ }
this._headers[header.toLowerCase()] = value;
}
open(method: string, url: string, async: ?boolean): void {
- /* Other optional arguments are not supported */
+ /* Other optional arguments are not supported yet */
if (this.readyState !== this.UNSENT) {
throw new Error('Cannot open, already sending');
}
@@ -85,10 +97,11 @@ class XMLHttpRequestBase {
// async is default
throw new Error('Synchronous http requests are not supported');
}
+ this._reset();
this._method = method;
this._url = url;
this._aborted = false;
- this._setReadyState(this.OPENED);
+ this.setReadyState(this.OPENED);
}
sendImpl(method: ?string, url: ?string, headers: Object, data: any): void {
@@ -111,20 +124,18 @@ class XMLHttpRequestBase {
}
abort(): void {
+ this._aborted = true;
this.abortImpl();
// only call onreadystatechange if there is something to abort,
// below logic is per spec
if (!(this.readyState === this.UNSENT ||
(this.readyState === this.OPENED && !this._sent) ||
this.readyState === this.DONE)) {
- this._sent = false;
- this._setReadyState(this.DONE);
+ this._reset();
+ this.setReadyState(this.DONE);
}
- if (this.readyState === this.DONE) {
- this._sendLoad();
- }
- this.readyState = this.UNSENT;
- this._aborted = true;
+ // Reset again after, in case modified in handler
+ this._reset();
}
callback(status: number, responseHeaders: ?Object, responseText: string): void {
@@ -132,18 +143,22 @@ class XMLHttpRequestBase {
return;
}
this.status = status;
- // Headers should be case-insensitive
- var lcResponseHeaders = {};
- for (var header in responseHeaders) {
- lcResponseHeaders[header.toLowerCase()] = responseHeaders[header];
- }
- this.responseHeaders = lcResponseHeaders;
+ this.setResponseHeaders(responseHeaders);
this.responseText = responseText;
- this._setReadyState(this.DONE);
- this._sendLoad();
+ this.setReadyState(this.DONE);
}
- _setReadyState(newState: number): void {
+ setResponseHeaders(responseHeaders: ?Object): void {
+ this.responseHeaders = responseHeaders || null;
+ var headers = responseHeaders || {};
+ this._lowerCaseResponseHeaders =
+ Object.keys(headers).reduce((lcaseHeaders, headerName) => {
+ lcaseHeaders[headerName.toLowerCase()] = headers[headerName];
+ return headers;
+ }, {});
+ }
+
+ setReadyState(newState: number): void {
this.readyState = newState;
// TODO: workaround flow bug with nullable function checks
var onreadystatechange = this.onreadystatechange;
@@ -152,6 +167,9 @@ class XMLHttpRequestBase {
// event anywhere, let's leave it empty
onreadystatechange(null);
}
+ if (newState === this.DONE && !this._aborted) {
+ this._sendLoad();
+ }
}
_sendLoad(): void {