diff --git a/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m b/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m index 20df842757a2b4..c92a505a9ec8fa 100644 --- a/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m +++ b/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m @@ -31,12 +31,12 @@ - (void)testSimpleCase { NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllParts:^(NSDictionary *headers, NSData *content, BOOL done) { + BOOL success = [reader readAllPartsWithCompletionCallback:^(NSDictionary *headers, NSData *content, BOOL done) { XCTAssertTrue(done); XCTAssertEqualObjects(headers[@"Content-Type"], @"application/json; charset=utf-8"); XCTAssertEqualObjects([[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding], @"{}"); count++; - }]; + } progressCallback: nil]; XCTAssertTrue(success); XCTAssertEqual(count, 1); } @@ -56,13 +56,13 @@ - (void)testMultipleParts { NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllParts:^(__unused NSDictionary *headers, NSData *content, BOOL done) { + BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, NSData *content, BOOL done) { count++; XCTAssertEqual(done, count == 3); NSString *expectedBody = [NSString stringWithFormat:@"%ld", (long)count]; NSString *actualBody = [[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding]; XCTAssertEqualObjects(actualBody, expectedBody); - }]; + } progressCallback:nil]; XCTAssertTrue(success); XCTAssertEqual(count, 3); } @@ -73,9 +73,9 @@ - (void)testNoDelimiter { NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllParts:^(__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { + BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { count++; - }]; + } progressCallback:nil]; XCTAssertFalse(success); XCTAssertEqual(count, 0); } @@ -93,9 +93,9 @@ - (void)testNoCloseDelimiter { NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllParts:^(__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { + BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { count++; - }]; + } progressCallback:nil]; XCTAssertFalse(success); XCTAssertEqual(count, 1); } diff --git a/React/Base/RCTJavaScriptLoader.mm b/React/Base/RCTJavaScriptLoader.mm index cbc686d9150464..4330e4e78b503c 100755 --- a/React/Base/RCTJavaScriptLoader.mm +++ b/React/Base/RCTJavaScriptLoader.mm @@ -198,7 +198,6 @@ static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoad return; } - RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) { if (!done) { if (onProgress) { @@ -261,6 +260,11 @@ static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoad } onComplete(nil, data, data.length); + } progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) { + // Only care about download progress events for the javascript bundle part. + if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) { + onProgress(progressEventFromDownloadProgress(loaded, total)); + } }]; [task startTask]; @@ -287,6 +291,16 @@ static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoad return progress; } +static RCTLoadingProgress *progressEventFromDownloadProgress(NSNumber *total, NSNumber *done) +{ + RCTLoadingProgress *progress = [RCTLoadingProgress new]; + progress.status = @"Downloading JavaScript bundle"; + // Progress values are in bytes transform them to kilobytes for smaller numbers. + progress.done = done != nil ? @([done integerValue] / 1024) : nil; + progress.total = total != nil ? @([total integerValue] / 1024) : nil; + return progress; +} + static NSDictionary *userInfoForRawResponse(NSString *rawText) { NSDictionary *parsedResponse = RCTJSONParse(rawText, nil); diff --git a/React/Base/RCTMultipartDataTask.h b/React/Base/RCTMultipartDataTask.h index 0d650b0d6afcf4..25f559e3d524be 100644 --- a/React/Base/RCTMultipartDataTask.h +++ b/React/Base/RCTMultipartDataTask.h @@ -15,7 +15,10 @@ typedef void (^RCTMultipartDataTaskCallback)(NSInteger statusCode, NSDictionary @interface RCTMultipartDataTask : NSObject -- (instancetype)initWithURL:(NSURL *)url partHandler:(RCTMultipartDataTaskCallback)partHandler; +- (instancetype)initWithURL:(NSURL *)url + partHandler:(RCTMultipartDataTaskCallback)partHandler + progressHandler:(RCTMultipartProgressCallback)progressHandler; + - (void)startTask; @end diff --git a/React/Base/RCTMultipartDataTask.m b/React/Base/RCTMultipartDataTask.m index 27feaffbb1e724..6ca63f3c4d8f16 100644 --- a/React/Base/RCTMultipartDataTask.m +++ b/React/Base/RCTMultipartDataTask.m @@ -30,17 +30,21 @@ static BOOL isStreamTaskSupported() { @implementation RCTMultipartDataTask { NSURL *_url; RCTMultipartDataTaskCallback _partHandler; + RCTMultipartProgressCallback _progressHandler; NSInteger _statusCode; NSDictionary *_headers; NSString *_boundary; NSMutableData *_data; } -- (instancetype)initWithURL:(NSURL *)url partHandler:(RCTMultipartDataTaskCallback)partHandler +- (instancetype)initWithURL:(NSURL *)url + partHandler:(RCTMultipartDataTaskCallback)partHandler + progressHandler:(RCTMultipartProgressCallback)progressHandler { if (self = [super init]) { _url = url; _partHandler = [partHandler copy]; + _progressHandler = [progressHandler copy]; } return self; } @@ -117,9 +121,9 @@ - (void)URLSession:(__unused NSURLSession *)session _partHandler = nil; NSInteger statusCode = _statusCode; - BOOL completed = [reader readAllParts:^(NSDictionary *headers, NSData *content, BOOL done) { + BOOL completed = [reader readAllPartsWithCompletionCallback:^(NSDictionary *headers, NSData *content, BOOL done) { partHandler(statusCode, headers, content, nil, done); - }]; + } progressCallback:_progressHandler]; if (!completed) { partHandler(statusCode, nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil], YES); } diff --git a/React/Base/RCTMultipartStreamReader.h b/React/Base/RCTMultipartStreamReader.h index ab77bd1ae85e28..30ffe5726d9ea8 100644 --- a/React/Base/RCTMultipartStreamReader.h +++ b/React/Base/RCTMultipartStreamReader.h @@ -10,6 +10,7 @@ #import typedef void (^RCTMultipartCallback)(NSDictionary *headers, NSData *content, BOOL done); +typedef void (^RCTMultipartProgressCallback)(NSDictionary *headers, NSNumber *loaded, NSNumber *total); // RCTMultipartStreamReader can be used to parse responses with Content-Type: multipart/mixed @@ -17,6 +18,7 @@ typedef void (^RCTMultipartCallback)(NSDictionary *headers, NSData *content, BOO @interface RCTMultipartStreamReader : NSObject - (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary; -- (BOOL)readAllParts:(RCTMultipartCallback)callback; +- (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback + progressCallback:(RCTMultipartProgressCallback)progressCallback; @end diff --git a/React/Base/RCTMultipartStreamReader.m b/React/Base/RCTMultipartStreamReader.m index 87b85c65e82220..6f4c5b046791a0 100644 --- a/React/Base/RCTMultipartStreamReader.m +++ b/React/Base/RCTMultipartStreamReader.m @@ -9,11 +9,14 @@ #import "RCTMultipartStreamReader.h" +#import + #define CRLF @"\r\n" @implementation RCTMultipartStreamReader { __strong NSInputStream *_stream; __strong NSString *_boundary; + CFTimeInterval _lastDownloadProgress; } - (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary @@ -21,6 +24,7 @@ - (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString * if (self = [super init]) { _stream = stream; _boundary = boundary; + _lastDownloadProgress = CACurrentMediaTime(); } return self; } @@ -42,12 +46,17 @@ - (NSDictionary *)parseHeaders:(NSData *)data return headers; } -- (void)emitChunk:(NSData *)data callback:(RCTMultipartCallback)callback done:(BOOL)done +- (void)emitChunk:(NSData *)data headers:(NSDictionary *)headers callback:(RCTMultipartCallback)callback done:(BOOL)done { NSData *marker = [CRLF CRLF dataUsingEncoding:NSUTF8StringEncoding]; NSRange range = [data rangeOfData:marker options:0 range:NSMakeRange(0, data.length)]; if (range.location == NSNotFound) { callback(nil, data, done); + } else if (headers != nil) { + // If headers were parsed already just use that to avoid doing it twice. + NSInteger bodyStart = range.location + marker.length; + NSData *bodyData = [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)]; + callback(headers, bodyData, done); } else { NSData *headersData = [data subdataWithRange:NSMakeRange(0, range.location)]; NSInteger bodyStart = range.location + marker.length; @@ -56,7 +65,26 @@ - (void)emitChunk:(NSData *)data callback:(RCTMultipartCallback)callback done:(B } } -- (BOOL)readAllParts:(RCTMultipartCallback)callback +- (void)emitProgress:(NSDictionary *)headers + contentLength:(NSUInteger)contentLength + final:(BOOL)final + callback:(RCTMultipartProgressCallback)callback +{ + if (headers == nil) { + return; + } + // Throttle progress events so we don't send more that around 60 per second. + CFTimeInterval currentTime = CACurrentMediaTime(); + + NSUInteger headersContentLength = headers[@"Content-Length"] != nil ? [headers[@"Content-Length"] unsignedIntValue] : 0; + if (callback && (currentTime - _lastDownloadProgress > 0.016 || final)) { + _lastDownloadProgress = currentTime; + callback(headers, @(headersContentLength), @(contentLength)); + } +} + +- (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback + progressCallback:(RCTMultipartProgressCallback)progressCallback { NSInteger chunkStart = 0; NSInteger bytesSeen = 0; @@ -64,6 +92,8 @@ - (BOOL)readAllParts:(RCTMultipartCallback)callback NSData *delimiter = [[NSString stringWithFormat:@"%@--%@%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding]; NSData *closeDelimiter = [[NSString stringWithFormat:@"%@--%@--%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding]; NSMutableData *content = [[NSMutableData alloc] initWithCapacity:1]; + NSDictionary *currentHeaders = nil; + NSUInteger currentHeadersLength = 0; const NSUInteger bufferLen = 4 * 1024; uint8_t buffer[bufferLen]; @@ -75,6 +105,8 @@ - (BOOL)readAllParts:(RCTMultipartCallback)callback // to allow for the edge case when the delimiter is cut by read call NSInteger searchStart = MAX(bytesSeen - (NSInteger)closeDelimiter.length, chunkStart); NSRange remainingBufferRange = NSMakeRange(searchStart, content.length - searchStart); + + // Check for delimiters. NSRange range = [content rangeOfData:delimiter options:0 range:remainingBufferRange]; if (range.location == NSNotFound) { isCloseDelimiter = YES; @@ -82,6 +114,23 @@ - (BOOL)readAllParts:(RCTMultipartCallback)callback } if (range.location == NSNotFound) { + if (currentHeaders == nil) { + // Check for the headers delimiter. + NSData *headersMarker = [CRLF CRLF dataUsingEncoding:NSUTF8StringEncoding]; + NSRange headersRange = [content rangeOfData:headersMarker options:0 range:remainingBufferRange]; + if (headersRange.location != NSNotFound) { + NSData *headersData = [content subdataWithRange:NSMakeRange(chunkStart, headersRange.location - chunkStart)]; + currentHeadersLength = headersData.length; + currentHeaders = [self parseHeaders:headersData]; + } + } else { + // When headers are loaded start sending progress callbacks. + [self emitProgress:currentHeaders + contentLength:content.length - currentHeadersLength + final:NO + callback:progressCallback]; + } + bytesSeen = content.length; NSInteger bytesRead = [_stream read:buffer maxLength:bufferLen]; if (bytesRead <= 0 || _stream.streamError) { @@ -98,7 +147,13 @@ - (BOOL)readAllParts:(RCTMultipartCallback)callback // Ignore preamble if (chunkStart > 0) { NSData *chunk = [content subdataWithRange:NSMakeRange(chunkStart, length)]; - [self emitChunk:chunk callback:callback done:isCloseDelimiter]; + [self emitProgress:currentHeaders + contentLength:chunk.length - currentHeadersLength + final:YES + callback:progressCallback]; + [self emitChunk:chunk headers:currentHeaders callback:callback done:isCloseDelimiter]; + currentHeaders = nil; + currentHeadersLength = 0; } if (isCloseDelimiter) {