-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Show bundle download progress on iOS #15066
Changes from 1 commit
0a0f115
16975fd
c3be763
afdc406
a6ee75b
4fbec2c
8096502
7d02bb2
f3d3e47
dc9f138
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,18 +9,22 @@ | |
|
||
#import "RCTMultipartStreamReader.h" | ||
|
||
#import <QuartzCore/CAAnimation.h> | ||
|
||
#define CRLF @"\r\n" | ||
|
||
@implementation RCTMultipartStreamReader { | ||
__strong NSInputStream *_stream; | ||
__strong NSString *_boundary; | ||
CFTimeInterval _lastDownloadProgress; | ||
} | ||
|
||
- (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary | ||
{ | ||
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,14 +65,34 @@ - (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)readAllParts:(RCTMultipartCallback)callback progressCallback:(RCTMultipartProgressCallback)progressCallback | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we changed the signature of this method, may be we can name it a bit better including There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe "CompletionCallback" so it's clear how it's different from the progress callback? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oups forgot to update the signature in tests, also updated it to |
||
{ | ||
NSInteger chunkStart = 0; | ||
NSInteger bytesSeen = 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]; | ||
NSDictionary *currentHeaders = nil; | ||
NSUInteger currentHeadersLength = 0; | ||
|
||
const NSUInteger bufferLen = 4 * 1024; | ||
uint8_t buffer[bufferLen]; | ||
|
@@ -75,13 +104,32 @@ - (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; | ||
range = [content rangeOfData:closeDelimiter options:0 range:remainingBufferRange]; | ||
} | ||
|
||
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 +146,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) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we convert to kilobytes here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly just to make the numbers smaller, we don't really have a way to show units with the current setup so it's no really clear what the numbers are anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we don't make them smaller right before displaying them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is as close as we get to displaying them, this is also used for showing the packager module transform progress so it just takes a progress number and the total then formats it to something like "10% (10/100)".