Add multipart response stream reader
Summary: Packager can take a long time to load and the progress is usually displayed in another window (Terminal). I'm adding support for showing a UI inside React Native app for packager's progress when loading a bundle. This is how it will work: 1. React Native sends request to packager with `Accept: multipart/mixed` header. 2. Packager will detect that header to detect that client supports progress events and will reply with `Content-Type: multipart/mixed`. 3. While building the bundle it will emit chunks with small metadata (like `{progress: 0.3}`). In the end it will send the last chunk with the content of the bundle. 4. RN runtime will be receiving the events, for each progress event it will update the UI. The last chunk will be the actual bundle which will end the download process. This workflow is totally backwards-compatible -- normally RN doesn't set the `Accept` header. Reviewed By: mmmulani Differential Revision: D3845684 fbshipit-source-id: 5b3d2c5a4c6f4718d7e5de060d98f17491e82aba
This commit is contained in:
parent
179a651240
commit
e2b25c8c9d
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
001BFCE41D838343008E587E /* RCTMultipartStreamReaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 001BFCE31D838343008E587E /* RCTMultipartStreamReaderTests.m */; };
|
||||
1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1300627E1B59179B0043FE5A /* RCTGzipTests.m */; };
|
||||
13129DD41C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13129DD31C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m */; };
|
||||
13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FE81AA91428003F314A /* libRCTImage.a */; };
|
||||
|
@ -196,6 +197,7 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
001BFCE31D838343008E587E /* RCTMultipartStreamReaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultipartStreamReaderTests.m; sourceTree = "<group>"; };
|
||||
004D289E1AAF61C70097A701 /* UIExplorerUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1300627E1B59179B0043FE5A /* RCTGzipTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGzipTests.m; sourceTree = "<group>"; };
|
||||
13129DD31C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleInitNotificationRaceTests.m; sourceTree = "<group>"; };
|
||||
|
@ -460,6 +462,7 @@
|
|||
134CB9291C85A38800265FA6 /* RCTModuleInitTests.m */,
|
||||
13129DD31C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m */,
|
||||
1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */,
|
||||
001BFCE31D838343008E587E /* RCTMultipartStreamReaderTests.m */,
|
||||
138D6A161B53CD440074A87E /* RCTShadowViewTests.m */,
|
||||
1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */,
|
||||
13BCE84E1C9C209600DD7AAD /* RCTComponentPropsTests.m */,
|
||||
|
@ -979,6 +982,7 @@
|
|||
1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */,
|
||||
13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */,
|
||||
1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */,
|
||||
001BFCE41D838343008E587E /* RCTMultipartStreamReaderTests.m in Sources */,
|
||||
13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */,
|
||||
138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */,
|
||||
13B6C1A31C34225900D3FAF5 /* RCTURLUtilsTests.m in Sources */,
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
#import "RCTUtils.h"
|
||||
#import "RCTMultipartStreamReader.h"
|
||||
|
||||
@interface RCTMultipartStreamReaderTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTMultipartStreamReaderTests
|
||||
|
||||
- (void)testSimpleCase {
|
||||
NSString *response =
|
||||
@"preable, should be ignored\r\n"
|
||||
@"--sample_boundary\r\n"
|
||||
@"Content-Type: application/json; charset=utf-8\r\n"
|
||||
@"Content-Length: 2\r\n\r\n"
|
||||
@"{}\r\n"
|
||||
@"--sample_boundary--\r\n"
|
||||
@"epilogue, should be ignored";
|
||||
|
||||
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) {
|
||||
XCTAssertTrue(done);
|
||||
XCTAssertEqualObjects(headers[@"Content-Type"], @"application/json; charset=utf-8");
|
||||
XCTAssertEqualObjects([[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding], @"{}");
|
||||
count++;
|
||||
}];
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertEqual(count, 1);
|
||||
}
|
||||
|
||||
- (void)testMultipleParts {
|
||||
NSString *response =
|
||||
@"preable, should be ignored\r\n"
|
||||
@"--sample_boundary\r\n"
|
||||
@"1\r\n"
|
||||
@"--sample_boundary\r\n"
|
||||
@"2\r\n"
|
||||
@"--sample_boundary\r\n"
|
||||
@"3\r\n"
|
||||
@"--sample_boundary--\r\n"
|
||||
@"epilogue, should be ignored";
|
||||
|
||||
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) {
|
||||
count++;
|
||||
XCTAssertEqual(done, count == 3);
|
||||
NSString *expectedBody = [NSString stringWithFormat:@"%ld", (long)count];
|
||||
NSString *actualBody = [[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding];
|
||||
XCTAssertEqualObjects(actualBody, expectedBody);
|
||||
}];
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertEqual(count, 3);
|
||||
}
|
||||
|
||||
- (void)testNoDelimiter {
|
||||
NSString *response = @"Yolo";
|
||||
|
||||
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) {
|
||||
count++;
|
||||
}];
|
||||
XCTAssertFalse(success);
|
||||
XCTAssertEqual(count, 0);
|
||||
}
|
||||
|
||||
- (void)testNoCloseDelimiter {
|
||||
NSString *response =
|
||||
@"preable, should be ignored\r\n"
|
||||
@"--sample_boundary\r\n"
|
||||
@"Content-Type: application/json; charset=utf-8\r\n"
|
||||
@"Content-Length: 2\r\n\r\n"
|
||||
@"{}\r\n"
|
||||
@"--sample_boundary\r\n"
|
||||
@"incomplete message...";
|
||||
|
||||
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) {
|
||||
count++;
|
||||
}];
|
||||
XCTAssertFalse(success);
|
||||
XCTAssertEqual(count, 1);
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef void (^RCTMultipartCallback)(NSDictionary *headers, NSData *content, BOOL done);
|
||||
|
||||
|
||||
// RCTMultipartStreamReader can be used to parse responses with Content-Type: multipart/mixed
|
||||
// See https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
|
||||
@interface RCTMultipartStreamReader : NSObject
|
||||
|
||||
- (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary;
|
||||
- (BOOL)readAllParts:(RCTMultipartCallback)callback;
|
||||
|
||||
@end
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#import "RCTMultipartStreamReader.h"
|
||||
|
||||
#define CRLF @"\r\n"
|
||||
|
||||
@implementation RCTMultipartStreamReader {
|
||||
__strong NSInputStream *_stream;
|
||||
__strong NSString *_boundary;
|
||||
}
|
||||
|
||||
- (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_stream = stream;
|
||||
_boundary = boundary;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSDictionary *)parseHeaders:(NSData *)data
|
||||
{
|
||||
NSMutableDictionary *headers = [NSMutableDictionary new];
|
||||
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
NSArray<NSString *> *lines = [text componentsSeparatedByString:CRLF];
|
||||
for (NSString *line in lines) {
|
||||
NSUInteger location = [line rangeOfString:@":"].location;
|
||||
if (location == NSNotFound) {
|
||||
continue;
|
||||
}
|
||||
NSString *key = [line substringToIndex:location];
|
||||
NSString *value = [[line substringFromIndex:location + 1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
[headers setValue:value forKey:key];
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
- (void)emitChunk:(NSData *)data 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 {
|
||||
NSData *headersData = [data subdataWithRange:NSMakeRange(0, range.location)];
|
||||
NSInteger bodyStart = range.location + marker.length;
|
||||
NSData *bodyData = [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)];
|
||||
callback([self parseHeaders:headersData], bodyData, done);
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)readAllParts:(RCTMultipartCallback)callback
|
||||
{
|
||||
NSInteger start = 0;
|
||||
NSData *delimiter = [[NSString stringWithFormat:@"%@--%@%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *closeDelimiter = [[NSString stringWithFormat:@"%@--%@--%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSMutableData *content = [[NSMutableData alloc] initWithCapacity:1];
|
||||
|
||||
const NSUInteger bufferLen = 4 * 1024;
|
||||
uint8_t buffer[bufferLen];
|
||||
|
||||
[_stream open];
|
||||
while (true) {
|
||||
BOOL isCloseDelimiter = NO;
|
||||
NSRange remainingBufferRange = NSMakeRange(start, content.length - start);
|
||||
NSRange range = [content rangeOfData:delimiter options:0 range:remainingBufferRange];
|
||||
if (range.location == NSNotFound) {
|
||||
isCloseDelimiter = YES;
|
||||
range = [content rangeOfData:closeDelimiter options:0 range:remainingBufferRange];
|
||||
}
|
||||
|
||||
if (range.location == NSNotFound) {
|
||||
NSInteger bytesRead = [_stream read:buffer maxLength:bufferLen];
|
||||
if (bytesRead <= 0 || _stream.streamError) {
|
||||
return NO;
|
||||
}
|
||||
[content appendBytes:buffer length:bytesRead];
|
||||
continue;
|
||||
}
|
||||
|
||||
NSInteger end = range.location;
|
||||
NSInteger length = end - start;
|
||||
|
||||
// Ignore preamble
|
||||
if (start > 0) {
|
||||
NSData *chunk = [content subdataWithRange:NSMakeRange(start, length)];
|
||||
[self emitChunk:chunk callback:callback done:isCloseDelimiter];
|
||||
}
|
||||
|
||||
if (isCloseDelimiter) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
start = end + delimiter.length;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; };
|
||||
001BFCD01D8381DE008E587E /* RCTMultipartStreamReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */; };
|
||||
008341F61D1DB34400876D9A /* RCTJSStackFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 008341F41D1DB34400876D9A /* RCTJSStackFrame.m */; };
|
||||
131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; };
|
||||
131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */; };
|
||||
|
@ -122,6 +123,8 @@
|
|||
/* Begin PBXFileReference section */
|
||||
000E6CE91AB0E97F000CDF4D /* RCTSourceCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSourceCode.h; sourceTree = "<group>"; };
|
||||
000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = "<group>"; };
|
||||
001BFCCE1D8381DE008E587E /* RCTMultipartStreamReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMultipartStreamReader.h; sourceTree = "<group>"; };
|
||||
001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultipartStreamReader.m; sourceTree = "<group>"; };
|
||||
008341F41D1DB34400876D9A /* RCTJSStackFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSStackFrame.m; sourceTree = "<group>"; };
|
||||
008341F51D1DB34400876D9A /* RCTJSStackFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSStackFrame.h; sourceTree = "<group>"; };
|
||||
131541CF1D3E4893006A0E08 /* CSSLayout-internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CSSLayout-internal.h"; sourceTree = "<group>"; };
|
||||
|
@ -614,6 +617,8 @@
|
|||
14C2CA731B3AC64300E6CBB2 /* RCTModuleData.mm */,
|
||||
14C2CA6F1B3AC63800E6CBB2 /* RCTModuleMethod.h */,
|
||||
14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */,
|
||||
001BFCCE1D8381DE008E587E /* RCTMultipartStreamReader.h */,
|
||||
001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */,
|
||||
13A6E20F1C19ABC700845B82 /* RCTNullability.h */,
|
||||
13A6E20C1C19AA0C00845B82 /* RCTParserUtils.h */,
|
||||
13A6E20D1C19AA0C00845B82 /* RCTParserUtils.m */,
|
||||
|
@ -730,6 +735,7 @@
|
|||
13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */,
|
||||
000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */,
|
||||
14A43DF31C20B1C900794BC8 /* RCTJSCProfiler.m in Sources */,
|
||||
001BFCD01D8381DE008E587E /* RCTMultipartStreamReader.m in Sources */,
|
||||
133CAE8E1B8E5CFD00F6AD92 /* RCTDatePicker.m in Sources */,
|
||||
14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */,
|
||||
13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */,
|
||||
|
|
Loading…
Reference in New Issue