react-native/React/Base/RCTJavaScriptLoader.m
Martín Bigio dd2415df73 seek & read RAM Bundle
Summary: For RAM bundling we don't want to hold the entire bundle in memory as that approach doesn't scale. Instead we want to seek and read individual sections as they're required. This rev does that by detecting the type of bundle we're dealing with by reading the first 4 bytes of it. If we're dealing with a RAM Bundle we bail loading.

Reviewed By: javache

Differential Revision: D3026205

fb-gh-sync-id: dc4c745d6f00aa7241203899e5ba136915efa6fe
shipit-source-id: dc4c745d6f00aa7241203899e5ba136915efa6fe
2016-03-17 10:35:28 -07:00

154 lines
5.6 KiB
Objective-C
Executable File

/**
* 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 "RCTJavaScriptLoader.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTSourceCode.h"
#import "RCTUtils.h"
#import "RCTPerformanceLogger.h"
#include <sys/stat.h>
uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5;
@implementation RCTJavaScriptLoader
RCT_NOT_IMPLEMENTED(- (instancetype)init)
+ (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete
{
// Sanitize the script URL
scriptURL = [RCTConvert NSURL:scriptURL.absoluteString];
if (!scriptURL) {
NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{
NSLocalizedDescriptionKey: @"No script URL provided."
}];
onComplete(error, nil);
return;
}
// Load local script file
if (scriptURL.fileURL) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
NSData *source = nil;
// Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle).
// The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`.
// The benefit of RAM bundle over a regular bundle is that we can lazily inject
// modules into JSC as they're required.
FILE *bundle = fopen(scriptURL.path.UTF8String, "r");
if (!bundle) {
onComplete(RCTErrorWithMessage([NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]), source);
return;
}
uint32_t magicNumber;
if (fread(&magicNumber, sizeof(magicNumber), 1, bundle) != 1) {
fclose(bundle);
onComplete(RCTErrorWithMessage(@"Error reading bundle"), source);
return;
}
magicNumber = NSSwapLittleIntToHost(magicNumber);
int32_t sourceLength = 0;
if (magicNumber == RCTRAMBundleMagicNumber) {
source = [NSData dataWithBytes:&magicNumber length:sizeof(magicNumber)];
struct stat statInfo;
if (stat(scriptURL.path.UTF8String, &statInfo) != 0) {
error = RCTErrorWithMessage(@"Error reading bundle");
} else {
sourceLength = statInfo.st_size;
}
} else {
source = [NSData dataWithContentsOfFile:scriptURL.path
options:NSDataReadingMappedIfSafe
error:&error];
sourceLength = source.length;
}
RCTPerformanceLoggerSet(RCTPLBundleSize, sourceLength);
fclose(bundle);
onComplete(error, source);
});
return;
}
// Load remote script file
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// Handle general request errors
if (error) {
if ([error.domain isEqualToString:NSURLErrorDomain]) {
NSString *desc = [@"Could not connect to development server.\n\nEnsure the following:\n- Node server is running and available on the same network - run 'npm start' from react-native root\n- Node server URL is correctly set in AppDelegate\n\nURL: " stringByAppendingString:scriptURL.absoluteString];
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: desc,
NSLocalizedFailureReasonErrorKey: error.localizedDescription,
NSUnderlyingErrorKey: error,
};
error = [NSError errorWithDomain:@"JSServer"
code:error.code
userInfo:userInfo];
}
onComplete(error, nil);
return;
}
// Parse response as text
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName != nil) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
if (cfEncoding != kCFStringEncodingInvalidId) {
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
// Handle HTTP errors
if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) {
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]] &&
[errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new];
for (NSDictionary *err in errorDetails[@"errors"]) {
[fakeStack addObject: @{
@"methodName": err[@"description"] ?: @"",
@"file": err[@"filename"] ?: @"",
@"lineNumber": err[@"lineNumber"] ?: @0
}];
}
userInfo = @{
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
@"stack": fakeStack,
};
} else {
userInfo = @{NSLocalizedDescriptionKey: rawText};
}
error = [NSError errorWithDomain:@"JSServer"
code:((NSHTTPURLResponse *)response).statusCode
userInfo:userInfo];
onComplete(error, nil);
return;
}
RCTPerformanceLoggerSet(RCTPLBundleSize, data.length);
onComplete(nil, data);
}];
[task resume];
}
@end