forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
5 changed files
with
244 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
Examples/UIExplorer/UIExplorerUnitTests/RCTMultipartStreamReaderTests.m
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters