From a2db4a4a5bb579848e0a9504a12f81d599895b53 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 1 Jun 2015 08:28:31 -0700 Subject: [PATCH 01/14] Removed redundant JSON encode/decode from RCTDataManager Summary: @public For some reason we were manually JSON-encoding the RCTDataManager responses, and then decoding them again on the JS side. Since all data sent over the bridge is JSON-encoded anyway, this is pretty pointless. Test Plan: * Test Movies app in OSS, which uses RCTDataManager * Test any code that uses RKHTTPQueryGenericExecutor to make network requests (e.g. Groups) * Test the Groups photo upload feature, which uses RKHTTPQueryWithImageUploadExecutor --- Examples/Movies/Movies/main.m | 2 +- Libraries/Network/RCTDataManager.m | 35 +++++++++++++------------ Libraries/Network/XMLHttpRequest.ios.js | 5 +--- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Examples/Movies/Movies/main.m b/Examples/Movies/Movies/main.m index 8954f343ce315c..9c58a39a483c81 100644 --- a/Examples/Movies/Movies/main.m +++ b/Examples/Movies/Movies/main.m @@ -17,6 +17,6 @@ int main(int argc, char * argv[]) { @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m index 1d0a793de8e454..6a2e39b2ddb86b 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -39,34 +39,35 @@ @implementation RCTDataManager // Build data task NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *connectionError) { + NSHTTPURLResponse *httpResponse = nil; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + // Might be a local file request + httpResponse = (NSHTTPURLResponse *)response; + } + // Build response - NSDictionary *responseJSON; + NSArray *responseJSON; if (connectionError == nil) { NSStringEncoding encoding = NSUTF8StringEncoding; if (response.textEncodingName) { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } - NSHTTPURLResponse *httpResponse = nil; - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { - // Might be a local file request - httpResponse = (NSHTTPURLResponse *)response; - } - responseJSON = @{ - @"status": @([httpResponse statusCode] ?: 200), - @"responseHeaders": [httpResponse allHeaderFields] ?: @{}, - @"responseText": [[NSString alloc] initWithData:data encoding:encoding] ?: @"" - }; + responseJSON = @[ + @(httpResponse.statusCode ?: 200), + httpResponse.allHeaderFields ?: @{}, + [[NSString alloc] initWithData:data encoding:encoding] ?: @"", + ]; } else { - responseJSON = @{ - @"status": @0, - @"responseHeaders": @{}, - @"responseText": [connectionError localizedDescription] ?: [NSNull null] - }; + responseJSON = @[ + @(httpResponse.statusCode), + httpResponse.allHeaderFields ?: @{}, + connectionError.localizedDescription ?: [NSNull null], + ]; } // Send response (won't be sent on same thread as caller) - responseSender(@[RCTJSONStringify(responseJSON, NULL)]); + responseSender(responseJSON); }]; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 6c7367c18db725..64b5ea526c2435 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -30,10 +30,7 @@ class XMLHttpRequest extends XMLHttpRequestBase { }, // TODO: Do we need this? is it used anywhere? 'h' + crc32(method + '|' + url + '|' + data), - (result) => { - result = JSON.parse(result); - this.callback(result.status, result.responseHeaders, result.responseText); - } + this.callback.bind(this) ); } From 49e87af934b8060858a8b0e3e1b03b690470b1e8 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 1 Jun 2015 08:34:09 -0700 Subject: [PATCH 02/14] Fixed text update on OSS --- Libraries/Text/RCTShadowText.h | 1 - Libraries/Text/RCTShadowText.m | 41 +++++++++++++++++++++++++++++++++ Libraries/Text/RCTTextManager.m | 2 -- React/Modules/RCTUIManager.m | 4 ++++ React/Views/RCTShadowView.h | 37 +++++++++++++++++++---------- React/Views/RCTShadowView.m | 30 +++++++++++++++--------- 6 files changed, 89 insertions(+), 26 deletions(-) diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index 189bff79e4c109..d156bb4d60a197 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -28,7 +28,6 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, strong) UIColor *textBackgroundColor; @property (nonatomic, assign) NSWritingDirection writingDirection; -- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width; - (void)recomputeText; @end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 7e1daf90855279..511697f8992e80 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -12,6 +12,8 @@ #import "RCTConvert.h" #import "RCTLog.h" #import "RCTShadowRawText.h" +#import "RCTSparseArray.h" +#import "RCTText.h" #import "RCTUtils.h" NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; @@ -19,6 +21,8 @@ @implementation RCTShadowText { + NSTextStorage *_cachedTextStorage; + CGFloat _cachedTextStorageWidth; NSAttributedString *_cachedAttributedString; CGFloat _effectiveLetterSpacing; } @@ -50,8 +54,35 @@ - (instancetype)init return self; } +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties +{ + parentProperties = [super processUpdatedProperties:applierBlocks + parentProperties:parentProperties]; + + NSTextStorage *textStorage = [self buildTextStorageForWidth:self.frame.size.width]; + [applierBlocks addObject:^(RCTSparseArray *viewRegistry) { + RCTText *view = viewRegistry[self.reactTag]; + view.textStorage = textStorage; + }]; + + return parentProperties; +} + +- (void)applyLayoutNode:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition +{ + [super applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; + [self dirtyPropagation]; +} + - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width { + if (_cachedTextStorage && width == _cachedTextStorageWidth) { + return _cachedTextStorage; + } + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString]; @@ -69,13 +100,23 @@ - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width [layoutManager addTextContainer:textContainer]; [layoutManager ensureLayoutForTextContainer:textContainer]; + _cachedTextStorage = textStorage; + _cachedTextStorageWidth = width; + return textStorage; } +- (void)dirtyText +{ + [super dirtyText]; + _cachedTextStorage = nil; +} + - (void)recomputeText { [self attributedString]; [self setTextComputed]; + [self dirtyPropagation]; } - (NSAttributedString *)attributedString diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index d9e547c773156c..26c6329e2313ec 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -96,12 +96,10 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowVie { NSNumber *reactTag = shadowView.reactTag; UIEdgeInsets padding = shadowView.paddingAsInsets; - NSTextStorage *textStorage = [shadowView buildTextStorageForWidth:shadowView.frame.size.width]; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTText *text = viewRegistry[reactTag]; text.contentInset = padding; - text.textStorage = textStorage; }; } diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 127cbd9fc5ee03..76e253a766a456 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -481,6 +481,10 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo shadowView.newView = NO; } + // These are blocks to be executed on each view, immediately after + // reactSetFrame: has been called. Note that if reactSetFrame: is not called, + // these won't be called either, so this is not a suitable place to update + // properties that aren't related to layout. NSMutableArray *updateBlocks = [[NSMutableArray alloc] init]; for (RCTShadowView *shadowView in viewsWithNewFrames) { RCTViewManager *manager = _viewManagerRegistry[shadowView.reactTag]; diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index c2e10750d3a5a8..1c44033f6df59f 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -20,8 +20,7 @@ typedef NS_ENUM(NSUInteger, RCTUpdateLifecycle) { RCTUpdateLifecycleDirtied, }; -// TODO: is this redundact now? -typedef void (^RCTApplierBlock)(RCTSparseArray *); +typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry); /** * ShadowView tree mirrors RCT view tree. Every node is highly stateful. @@ -117,34 +116,48 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); * The applierBlocks set contains RCTApplierBlock functions that must be applied * on the main thread in order to update the view. */ -- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties; +- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties; + +/** + * Process the updated properties and apply them to view. Shadow view classes + * that add additional propagating properties should override this method. + */ +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties NS_REQUIRES_SUPER; /** * Calculate all views whose frame needs updating after layout has been calculated. * The viewsWithNewFrame set contains the reactTags of the views that need updating. */ -- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame parentConstraint:(CGSize)parentConstraint; +- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame + parentConstraint:(CGSize)parentConstraint; + +/** + * Recursively apply layout to children. + */ +- (void)applyLayoutNode:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; /** * The following are implementation details exposed to subclasses. Do not call them directly */ -- (void)fillCSSNode:(css_node_t *)node; -- (void)dirtyLayout; +- (void)fillCSSNode:(css_node_t *)node NS_REQUIRES_SUPER; +- (void)dirtyLayout NS_REQUIRES_SUPER; - (BOOL)isLayoutDirty; -// TODO: is this still needed? -- (void)dirtyPropagation; +- (void)dirtyPropagation NS_REQUIRES_SUPER; - (BOOL)isPropagationDirty; -// TODO: move this to text node? -- (void)dirtyText; +- (void)dirtyText NS_REQUIRES_SUPER; +- (void)setTextComputed NS_REQUIRES_SUPER; - (BOOL)isTextDirty; -- (void)setTextComputed; /** * Triggers a recalculation of the shadow view's layout. */ -- (void)updateLayout; +- (void)updateLayout NS_REQUIRES_SUPER; /** * Computes the recursive offset, meaning the sum of all descendant offsets - diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index b26193f24ca946..ba70ca34f6558f 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -120,7 +120,9 @@ - (void)fillCSSNode:(css_node_t *)node // width = 213.5 - 106.5 = 107 // You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. -- (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition +- (void)applyLayoutNode:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition { if (!node->layout.should_update) { return; @@ -161,12 +163,19 @@ - (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)vie for (int i = 0; i < node->children_count; ++i) { RCTShadowView *child = (RCTShadowView *)_reactSubviews[i]; - [child applyLayoutNode:node->get_child(node->context, i) viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; + [child applyLayoutNode:node->get_child(node->context, i) + viewsWithNewFrame:viewsWithNewFrame + absolutePosition:absolutePosition]; } } -- (NSDictionary *)processBackgroundColor:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties { + // TODO: we always refresh all propagated properties when propagation is + // dirtied, but really we should track which properties have changed and + // only update those. + if (!_backgroundColor) { UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp]; if (parentBackgroundColor) { @@ -190,14 +199,15 @@ - (NSDictionary *)processBackgroundColor:(NSMutableSet *)applierBlocks parentPro return parentProperties; } -- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties +- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks + parentProperties:(NSDictionary *)parentProperties { if (_propagationLifecycle == RCTUpdateLifecycleComputed && [parentProperties isEqualToDictionary:_lastParentProperties]) { return; } _propagationLifecycle = RCTUpdateLifecycleComputed; _lastParentProperties = parentProperties; - NSDictionary *nextProps = [self processBackgroundColor:applierBlocks parentProperties:parentProperties]; + NSDictionary *nextProps = [self processUpdatedProperties:applierBlocks parentProperties:parentProperties]; for (RCTShadowView *child in _reactSubviews) { [child collectUpdatedProperties:applierBlocks parentProperties:nextProps]; } @@ -212,21 +222,19 @@ - (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame parentConstra - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor { - CGFloat totalOffsetTop = 0.0; - CGFloat totalOffsetLeft = 0.0; - CGSize size = self.frame.size; + CGPoint offset = CGPointZero; NSInteger depth = 30; // max depth to search RCTShadowView *shadowView = self; while (depth && shadowView && shadowView != ancestor) { - totalOffsetTop += shadowView.frame.origin.y; - totalOffsetLeft += shadowView.frame.origin.x; + offset.x += shadowView.frame.origin.x; + offset.y += shadowView.frame.origin.y; shadowView = shadowView->_superview; depth--; } if (ancestor != shadowView) { return CGRectNull; } - return (CGRect){{totalOffsetLeft, totalOffsetTop}, size}; + return (CGRect){offset, self.frame.size}; } - (instancetype)init From 57ce9fb11a352dfaa6f08145cf9159fec0224bdb Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Mon, 1 Jun 2015 09:27:30 -0700 Subject: [PATCH 03/14] [package.json] Leave the protocol choice for a Github repo to npm Summary: Fixes #1371 Closes https://github.com/facebook/react-native/pull/1447 Github Author: Brent Vatne Test Plan: Imported from GitHub, without a `Test Plan:` line. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 535d59a57300e8..19226bc0851bc6 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,9 @@ "react-timer-mixin": "^0.13.1", "react-tools": "0.13.2", "rebound": "^0.0.12", - "sane": "git://github.com/tadeuzagallo/sane.git#a029f8b04a", + "sane": "tadeuzagallo/sane#a029f8b04a", "source-map": "0.1.31", - "stacktrace-parser": "git://github.com/frantic/stacktrace-parser.git#493c5e5638", + "stacktrace-parser": "frantic/stacktrace-parser#493c5e5638", "uglify-js": "~2.4.16", "underscore": "1.7.0", "worker-farm": "^1.3.1", From 2b4daf228d2f99bb36fbbc7458b7e53f462f8d82 Mon Sep 17 00:00:00 2001 From: Felix Oghina Date: Mon, 1 Jun 2015 10:19:25 -0700 Subject: [PATCH 04/14] Expose fontScale to JS --- Libraries/Utilities/Dimensions.js | 1 + Libraries/Utilities/PixelRatio.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index b93000a33a8f50..31fa0d4dc4f3a6 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -29,6 +29,7 @@ if (dimensions && dimensions.windowPhysicalPixels) { width: windowPhysicalPixels.width / windowPhysicalPixels.scale, height: windowPhysicalPixels.height / windowPhysicalPixels.scale, scale: windowPhysicalPixels.scale, + fontScale: windowPhysicalPixels.fontScale, }; // delete so no callers rely on this existing diff --git a/Libraries/Utilities/PixelRatio.js b/Libraries/Utilities/PixelRatio.js index a3e4d9e7703aaf..7660fad3055409 100644 --- a/Libraries/Utilities/PixelRatio.js +++ b/Libraries/Utilities/PixelRatio.js @@ -59,6 +59,20 @@ class PixelRatio { return Dimensions.get('window').scale; } + /** + * Returns the scaling factor for font sizes. This is the ratio that is used to calculate the + * absolute font size, so any elements that heavily depend on that should use this to do + * calculations. + * + * If a font scale is not set, this returns the device pixel ratio. + * + * Currently this is only implemented on Android and reflects the user preference set in + * Settings > Display > Font size, on iOS it will always return the default pixel ratio. + */ + static getFontScale(): number { + return Dimensions.get('window').fontScale || PixelRatio.get(); + } + /** * Converts a layout size (dp) to pixel size (px). * From d548c85da6ce7cdefe650ff2ef0464652fe8e93a Mon Sep 17 00:00:00 2001 From: James Ide Date: Mon, 1 Jun 2015 09:47:41 -0700 Subject: [PATCH 05/14] [Bridge] Add support for JS async functions to RCT_EXPORT_METHOD Summary: Adds support for JS async methods and helps guide people writing native modules w.r.t. the callbacks. With this diff, on the native side you write: ```objc RCT_EXPORT_METHOD(getValueAsync:(NSString *)key resolver:(RCTPromiseResolver)resolve rejecter:(RCTPromiseRejecter)reject) { NSError *error = nil; id value = [_nativeDataStore valueForKey:key error:&error]; // "resolve" and "reject" are automatically defined blocks that take // any object (nil is OK) and an NSError, respectively if (!error) { resolve(value); } else { reject(error); } } ``` On the JS side, you can write: ```js var {DemoDataStore} = require('react-native').NativeModules; DemoDataStore.getValueAsync('sample-key').then((value) => { console.log('Got:', value); }, (error) => { console.error(error); // "error" is an Error object whose message is the NSError's description. // The NSError's code and domain are also set, and the native trace i Closes https://github.com/facebook/react-native/pull/1232 Github Author: James Ide Test Plan: Imported from GitHub, without a `Test Plan:` line. --- .../BatchedBridgeFactory.js | 68 ++++++++++---- Libraries/Utilities/MessageQueue.js | 14 +-- React/Base/RCTBridge.m | 89 ++++++++++++++++++- React/Base/RCTBridgeModule.h | 40 ++++++++- 4 files changed, 181 insertions(+), 30 deletions(-) diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js index dfc09ba7cbc219..4702e246de9eed 100644 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js +++ b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js @@ -19,9 +19,17 @@ var slice = Array.prototype.slice; var MethodTypes = keyMirror({ remote: null, + remoteAsync: null, local: null, }); +type ErrorData = { + message: string; + domain: string; + code: number; + nativeStackIOS?: string; +}; + /** * Creates remotely invokable modules. */ @@ -36,21 +44,40 @@ var BatchedBridgeFactory = { */ _createBridgedModule: function(messageQueue, moduleConfig, moduleName) { var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) { - return methodConfig.type === MethodTypes.local ? null : function() { - var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null; - var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; - var hasSuccCB = typeof lastArg === 'function'; - var hasErrorCB = typeof secondLastArg === 'function'; - hasErrorCB && invariant( - hasSuccCB, - 'Cannot have a non-function arg after a function arg.' - ); - var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0); - var args = slice.call(arguments, 0, arguments.length - numCBs); - var onSucc = hasSuccCB ? lastArg : null; - var onFail = hasErrorCB ? secondLastArg : null; - return messageQueue.call(moduleName, memberName, args, onFail, onSucc); - }; + switch (methodConfig.type) { + case MethodTypes.remote: + return function() { + var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null; + var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; + var hasErrorCB = typeof lastArg === 'function'; + var hasSuccCB = typeof secondLastArg === 'function'; + hasSuccCB && invariant( + hasErrorCB, + 'Cannot have a non-function arg after a function arg.' + ); + var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0); + var args = slice.call(arguments, 0, arguments.length - numCBs); + var onSucc = hasSuccCB ? secondLastArg : null; + var onFail = hasErrorCB ? lastArg : null; + messageQueue.call(moduleName, memberName, args, onSucc, onFail); + }; + + case MethodTypes.remoteAsync: + return function(...args) { + return new Promise((resolve, reject) => { + messageQueue.call(moduleName, memberName, args, resolve, (errorData) => { + var error = _createErrorFromErrorData(errorData); + reject(error); + }); + }); + }; + + case MethodTypes.local: + return null; + + default: + throw new Error('Unknown bridge method type: ' + methodConfig.type); + } }); for (var constName in moduleConfig.constants) { warning(!remoteModule[constName], 'saw constant and method named %s', constName); @@ -59,7 +86,6 @@ var BatchedBridgeFactory = { return remoteModule; }, - create: function(MessageQueue, modulesConfig, localModulesConfig) { var messageQueue = new MessageQueue(modulesConfig, localModulesConfig); return { @@ -80,4 +106,14 @@ var BatchedBridgeFactory = { } }; +function _createErrorFromErrorData(errorData: ErrorData): Error { + var { + message, + ...extraErrorInfo, + } = errorData; + var error = new Error(message); + error.framesToPop = 1; + return Object.assign(error, extraErrorInfo); +} + module.exports = BatchedBridgeFactory; diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index df34dde0629f31..5b1989c287f501 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -431,14 +431,14 @@ var MessageQueueMixin = { }, /** - * @param {Function} onFail Function to store in current thread for later - * lookup, when request fails. * @param {Function} onSucc Function to store in current thread for later * lookup, when request succeeds. + * @param {Function} onFail Function to store in current thread for later + * lookup, when request fails. * @param {Object?=} scope Scope to invoke `cb` with. * @param {Object?=} res Resulting callback ids. Use `this._POOLED_CBIDS`. */ - _storeCallbacksInCurrentThread: function(onFail, onSucc, scope) { + _storeCallbacksInCurrentThread: function(onSucc, onFail, scope) { invariant(onFail || onSucc, INTERNAL_ERROR); this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS); var succCBID = this._POOLED_CBIDS.successCallbackID; @@ -494,7 +494,7 @@ var MessageQueueMixin = { return ret; }, - call: function(moduleName, methodName, params, onFail, onSucc, scope) { + call: function(moduleName, methodName, params, onSucc, onFail, scope) { invariant( (!onFail || typeof onFail === 'function') && (!onSucc || typeof onSucc === 'function'), @@ -502,10 +502,10 @@ var MessageQueueMixin = { ); // Store callback _before_ sending the request, just in case the MailBox // returns the response in a blocking manner. - if (onSucc) { - this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS); + if (onSucc || onFail) { + this._storeCallbacksInCurrentThread(onSucc, onFail, scope, this._POOLED_CBIDS); + onSucc && params.push(this._POOLED_CBIDS.successCallbackID); onFail && params.push(this._POOLED_CBIDS.errorCallbackID); - params.push(this._POOLED_CBIDS.successCallbackID); } var moduleID = this._remoteModuleNameToModuleID[moduleName]; if (moduleID === undefined || moduleID === null) { diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 47c7f59420c91d..c1028a3fe700d2 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -47,6 +47,11 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; +typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { + RCTJavaScriptFunctionKindNormal, + RCTJavaScriptFunctionKindAsync, +}; + #ifdef __LP64__ typedef uint64_t RCTHeaderValue; typedef struct section_64 RCTHeaderSection; @@ -204,6 +209,27 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { return RCTModuleClassesByID; } +// TODO: Can we just replace RCTMakeError with this function instead? +static NSDictionary *RCTJSErrorFromNSError(NSError *error) +{ + NSString *errorMessage; + NSArray *stackTrace = [NSThread callStackSymbols]; + NSMutableDictionary *errorInfo = + [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; + + if (error) { + errorMessage = error.localizedDescription ?: @"Unknown error from a native module"; + errorInfo[@"domain"] = error.domain ?: RCTErrorDomain; + errorInfo[@"code"] = @(error.code); + } else { + errorMessage = @"Unknown error from a native module"; + errorInfo[@"domain"] = RCTErrorDomain; + errorInfo[@"code"] = @-1; + } + + return RCTMakeError(errorMessage, nil, errorInfo); +} + @class RCTBatchedBridge; @interface RCTBridge () @@ -239,6 +265,7 @@ @interface RCTModuleMethod : NSObject @property (nonatomic, copy, readonly) NSString *moduleClassName; @property (nonatomic, copy, readonly) NSString *JSMethodName; @property (nonatomic, assign, readonly) SEL selector; +@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind; @end @@ -420,6 +447,50 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { addBlockArgument(); useFallback = NO; + } else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) { + RCTAssert(i == numberOfArguments - 2, + @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", + _moduleClassName, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseResolveBlock value = (^(id result) { + NSArray *arguments = result ? @[result] : @[]; + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, arguments] + context:context]; + }); + ) + useFallback = NO; + _functionKind = RCTJavaScriptFunctionKindAsync; + } else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) { + RCTAssert(i == numberOfArguments - 1, + @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", + _moduleClassName, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { + NSDictionary *errorJSON = RCTJSErrorFromNSError(error); + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, @[errorJSON]] + context:context]; + }); + ) + useFallback = NO; + _functionKind = RCTJavaScriptFunctionKindAsync; } } @@ -498,9 +569,18 @@ - (void)invokeWithBridge:(RCTBridge *)bridge // Safety check if (arguments.count != _argumentBlocks.count) { + NSInteger actualCount = arguments.count; + NSInteger expectedCount = _argumentBlocks.count; + + // Subtract the implicit Promise resolver and rejecter functions for implementations of async functions + if (_functionKind == RCTJavaScriptFunctionKindAsync) { + actualCount -= 2; + expectedCount -= 2; + } + RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, - arguments.count, _argumentBlocks.count); + actualCount, expectedCount); return; } } @@ -525,7 +605,8 @@ - (void)invokeWithBridge:(RCTBridge *)bridge - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName]; + return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", + NSStringFromClass(self.class), self, _methodName, _JSMethodName]; } @end @@ -606,7 +687,7 @@ - (NSString *)description * }, * "methodName2": { * "methodID": 1, - * "type": "remote" + * "type": "remoteAsync" * }, * etc... * }, @@ -630,7 +711,7 @@ - (NSString *)description [methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) { methodsByName[method.JSMethodName] = @{ @"methodID": @(methodID), - @"type": @"remote", + @"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote", }; }]; diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 34b861ff3f8f10..1528b8cd99d8ef 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -17,6 +17,20 @@ */ typedef void (^RCTResponseSenderBlock)(NSArray *response); +/** + * Block that bridge modules use to resolve the JS promise waiting for a result. + * Nil results are supported and are converted to JS's undefined value. + */ +typedef void (^RCTPromiseResolveBlock)(id result); + +/** + * Block that bridge modules use to reject the JS promise waiting for a result. + * The error may be nil but it is preferable to pass an NSError object for more + * precise error messages. + */ +typedef void (^RCTPromiseRejectBlock)(NSError *error); + + /** * This constant can be returned from +methodQueue to force module * methods to be called on the JavaScript thread. This can have serious @@ -37,7 +51,7 @@ extern const dispatch_queue_t RCTJSThread; * A reference to the RCTBridge. Useful for modules that require access * to bridge features, such as sending events or making JS calls. This * will be set automatically by the bridge when it initializes the module. -* To implement this in your module, just add @synthesize bridge = _bridge; + * To implement this in your module, just add @synthesize bridge = _bridge; */ @property (nonatomic, weak) RCTBridge *bridge; @@ -70,6 +84,26 @@ extern const dispatch_queue_t RCTJSThread; * { ... } * * and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`. + * + * ## Promises + * + * Bridge modules can also define methods that are exported to JavaScript as + * methods that return a Promise, and are compatible with JS async functions. + * + * Declare the last two parameters of your native method to be a resolver block + * and a rejecter block. The resolver block must precede the rejecter block. + * + * For example: + * + * RCT_EXPORT_METHOD(doSomethingAsync:(NSString *)aString + * resolver:(RCTPromiseResolveBlock)resolve + * rejecter:(RCTPromiseRejectBlock)reject + * { ... } + * + * Calling `NativeModules.ModuleName.doSomethingAsync(aString)` from + * JavaScript will return a promise that is resolved or rejected when your + * native method implementation calls the respective block. + * */ #define RCT_EXPORT_METHOD(method) \ RCT_REMAP_METHOD(, method) @@ -118,7 +152,7 @@ extern const dispatch_queue_t RCTJSThread; RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername) /** - * Similar to RCT_EXTERN_MODULE but allows setting a custom JavaScript name + * Like RCT_EXTERN_MODULE, but allows setting a custom JavaScript name. */ #define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \ objc_name : objc_supername \ @@ -136,7 +170,7 @@ extern const dispatch_queue_t RCTJSThread; RCT_EXTERN_REMAP_METHOD(, method) /** - * Similar to RCT_EXTERN_REMAP_METHOD but allows setting a custom JavaScript name + * Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name. */ #define RCT_EXTERN_REMAP_METHOD(js_name, method) \ - (void)__rct_export__##method { \ From d723e176292c4398250d6ee0da64b4ce0d223d4d Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Mon, 1 Jun 2015 12:09:52 -0700 Subject: [PATCH 06/14] [ReactNative] Copy assets to corresponding folders on Android --- Libraries/Image/__tests__/resolveAssetSource-test.js | 2 +- Libraries/Image/resolveAssetSource.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index 866cf036880a19..5854dae0bd1263 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -175,7 +175,7 @@ describe('resolveAssetSource', () => { isStatic: true, width: 100, height: 200, - uri: 'assets_awesomemodule_subdir_logo1_', + uri: 'awesomemodule_subdir_logo1_', }); }); }); diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index 26592195d0a4a8..301d70dd9ac53a 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -50,7 +50,8 @@ function getPathInArchive(asset) { return (assetDir + '/' + asset.name) .toLowerCase() .replace(/\//g, '_') // Encode folder structure in file name - .replace(/([^a-z0-9_])/g, ''); // Remove illegal chars + .replace(/([^a-z0-9_])/g, '') // Remove illegal chars + .replace(/^assets_/, ''); // Remove "assets_" prefix } else { // E.g. 'assets/AwesomeModule/icon@2x.png' return getScaledAssetPath(asset); From 34cef28a10b7d44be453c072578b5b6c07a0dcd8 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 1 Jun 2015 13:27:21 -0700 Subject: [PATCH 07/14] [ReactNative] kill setInterval in ListView --- Libraries/CustomComponents/ListView/ListView.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 17f1e477dddb75..717f03847bc94c 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -45,7 +45,6 @@ var DEFAULT_INITIAL_ROWS = 10; var DEFAULT_SCROLL_RENDER_AHEAD = 1000; var DEFAULT_END_REACHED_THRESHOLD = 1000; var DEFAULT_SCROLL_CALLBACK_THROTTLE = 50; -var RENDER_INTERVAL = 20; var SCROLLVIEW_REF = 'listviewscroll'; @@ -258,7 +257,6 @@ var ListView = React.createClass({ // the component is laid out this.requestAnimationFrame(() => { this._measureAndUpdateScrollProps(); - this.setInterval(this._renderMoreRowsIfNeeded, RENDER_INTERVAL); }); }, @@ -329,7 +327,7 @@ var ListView = React.createClass({ totalIndex++; if (this.props.renderSeparator && - (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length -1)) { + (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)) { var adjacentRowHighlighted = this.state.highlightedRow.sectionID === sectionID && ( this.state.highlightedRow.rowID === rowID || @@ -397,6 +395,7 @@ var ListView = React.createClass({ _setScrollVisibleHeight: function(left, top, width, height) { this.scrollProperties.visibleHeight = height; this._updateVisibleRows(); + this._renderMoreRowsIfNeeded(); }, _renderMoreRowsIfNeeded: function() { @@ -443,8 +442,8 @@ var ListView = React.createClass({ } var updatedFrames = e && e.nativeEvent.updatedChildFrames; if (updatedFrames) { - updatedFrames.forEach((frame) => { - this._childFrames[frame.index] = merge(frame); + updatedFrames.forEach((newFrame) => { + this._childFrames[newFrame.index] = merge(newFrame); }); } var dataSource = this.props.dataSource; From 43adb7b02c49c3a05c3d4f884c1165303556e372 Mon Sep 17 00:00:00 2001 From: Joshua Sierles Date: Mon, 1 Jun 2015 15:46:06 -0700 Subject: [PATCH 08/14] [CameraRoll] support fetching videos from the camera roll Summary: This adds a parameter for fetching videos from the camera roll. It also changes the default to fetch both videos and photos. Closes https://github.com/facebook/react-native/pull/774 Github Author: Joshua Sierles Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Examples/UIExplorer/CameraRollView.ios.js | 15 ++++++++++++++- Libraries/CameraRoll/CameraRoll.js | 15 +++++++++++++++ Libraries/Image/RCTCameraRollManager.m | 12 +++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Examples/UIExplorer/CameraRollView.ios.js b/Examples/UIExplorer/CameraRollView.ios.js index 87dd23e6decf13..74507aa2c1b50e 100644 --- a/Examples/UIExplorer/CameraRollView.ios.js +++ b/Examples/UIExplorer/CameraRollView.ios.js @@ -59,6 +59,16 @@ var propTypes = { * imagesPerRow: Number of images to be shown in each row. */ imagesPerRow: React.PropTypes.number, + + /** + * The asset type, one of 'Photos', 'Videos' or 'All' + */ + assetType: React.PropTypes.oneOf([ + 'Photos', + 'Videos', + 'All', + ]), + }; var CameraRollView = React.createClass({ @@ -69,6 +79,7 @@ var CameraRollView = React.createClass({ groupTypes: 'SavedPhotos', batchSize: 5, imagesPerRow: 1, + assetType: 'Photos', renderImage: function(asset) { var imageSize = 150; var imageStyle = [styles.image, {width: imageSize, height: imageSize}]; @@ -89,6 +100,7 @@ var CameraRollView = React.createClass({ assets: ([]: Array), groupTypes: this.props.groupTypes, lastCursor: (null : ?string), + assetType: this.props.assetType, noMore: false, loadingMore: false, dataSource: ds, @@ -124,7 +136,8 @@ var CameraRollView = React.createClass({ var fetchParams: Object = { first: this.props.batchSize, - groupTypes: this.props.groupTypes + groupTypes: this.props.groupTypes, + assetType: this.props.assetType, }; if (this.state.lastCursor) { fetchParams.after = this.state.lastCursor; diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index 0d008ae75a7abe..67fa5083048dfd 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -29,8 +29,16 @@ var GROUP_TYPES_OPTIONS = [ 'SavedPhotos', // default ]; +var ASSET_TYPE_OPTIONS = [ + 'All', + 'Videos', + 'Photos', // default +]; + + // Flow treats Object and Array as disjoint types, currently. deepFreezeAndThrowOnMutationInDev((GROUP_TYPES_OPTIONS: any)); +deepFreezeAndThrowOnMutationInDev((ASSET_TYPE_OPTIONS: any)); /** * Shape of the param arg for the `getPhotos` function. @@ -58,6 +66,11 @@ var getPhotosParamChecker = createStrictShapeTypeChecker({ * titles. */ groupName: ReactPropTypes.string, + + /** + * Specifies filter on asset type + */ + assetType: ReactPropTypes.oneOf(ASSET_TYPE_OPTIONS), }); /** @@ -94,6 +107,7 @@ var getPhotosReturnChecker = createStrictShapeTypeChecker({ class CameraRoll { static GroupTypesOptions: Array; + static AssetTypeOptions: Array; /** * Saves the image with tag `tag` to the camera roll. * @@ -154,5 +168,6 @@ class CameraRoll { } CameraRoll.GroupTypesOptions = GROUP_TYPES_OPTIONS; +CameraRoll.AssetTypeOptions = ASSET_TYPE_OPTIONS; module.exports = CameraRoll; diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m index 8e6c8a532b87bb..d7b42f88560aba 100644 --- a/Libraries/Image/RCTCameraRollManager.m +++ b/Libraries/Image/RCTCameraRollManager.m @@ -69,7 +69,9 @@ - (void)callCallback:(RCTResponseSenderBlock)callback withAssets:(NSArray *)asse NSString *afterCursor = params[@"after"]; NSString *groupTypesStr = params[@"groupTypes"]; NSString *groupName = params[@"groupName"]; + NSString *assetType = params[@"assetType"]; ALAssetsGroupType groupTypes; + if ([groupTypesStr isEqualToString:@"Album"]) { groupTypes = ALAssetsGroupAlbum; } else if ([groupTypesStr isEqualToString:@"All"]) { @@ -93,7 +95,15 @@ - (void)callCallback:(RCTResponseSenderBlock)callback withAssets:(NSArray *)asse [[RCTImageLoader assetsLibrary] enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) { - [group setAssetsFilter:ALAssetsFilter.allPhotos]; + + if (assetType == nil || [assetType isEqualToString:@"Photos"]) { + [group setAssetsFilter:ALAssetsFilter.allPhotos]; + } else if ([assetType isEqualToString:@"Videos"]) { + [group setAssetsFilter:ALAssetsFilter.allVideos]; + } else if ([assetType isEqualToString:@"All"]) { + [group setAssetsFilter:ALAssetsFilter.allAssets]; + } + [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { if (result) { NSString *uri = [(NSURL *)[result valueForProperty:ALAssetPropertyAssetURL] absoluteString]; From 38f57ee18c44f68c1a1745ed81394481e1275ae9 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 1 Jun 2015 16:04:16 -0700 Subject: [PATCH 09/14] [ReactNative] improve UIExplorer keyboard interactions --- Examples/UIExplorer/UIExplorerList.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index fdbda4dc8d098e..a030220cabb1af 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -30,7 +30,7 @@ var { var { TestModule } = React.addons; var Settings = require('Settings'); -import type { Example, ExampleModule } from 'ExampleTypes'; +import type { ExampleModule } from 'ExampleTypes'; var createExamplePage = require('./createExamplePage'); @@ -154,7 +154,9 @@ class UIExplorerList extends React.Component { dataSource={this.state.dataSource} renderRow={this._renderRow.bind(this)} renderSectionHeader={this._renderSectionHeader} + keyboardShouldPersistTaps={true} automaticallyAdjustContentInsets={false} + keyboardDismissMode="onDrag" /> ); From 2a6fe079c08231389b56370a027f851258578770 Mon Sep 17 00:00:00 2001 From: James Ide Date: Mon, 1 Jun 2015 15:58:22 -0700 Subject: [PATCH 10/14] [Timers] Batch setImmediate handlers Summary: Wraps the setImmediate handlers in a `batchUpdates` call before they are synchronously executed at the end of the JS execution loop. Closes https://github.com/facebook/react-native/pull/1242 Github Author: James Ide Test Plan: Added two `setImmediate` calls to `componentDidMount` in UIExplorerApp. Each handler calls `setState`, and `componentWillUpdate` logs its state. With this diff, we can see the state updates are successfully batched. ```javascript componentDidMount() { setImmediate(() => { console.log('immediate 1'); this.setState({a: 1}); }); setImmediate(() => { console.log('immediate 2'); this.setState({a: 2}); }); }, componentWillUpdate(nextProps, nextState) { console.log('componentWillUpdate with next state.a =', nextState.a); }, ``` **Before:** "immediate 1" "componentWillUpdate with next state.a =", 1 "immediate 2" "componentWillUpdate with next state.a =", 2 **After:** "immediate 1" "immediate 2" "componentWillUpdate with next state.a =", 2 Addresses the batching issue in #1232. cc @vjeux @spicyj --- Libraries/Utilities/MessageQueue.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 5b1989c287f501..2ff8b81d783be7 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -472,8 +472,10 @@ var MessageQueueMixin = { }, _flushedQueueUnguarded: function() { - // Call the functions registred via setImmediate - JSTimersExecution.callImmediates(); + ReactUpdates.batchedUpdates(() => { + // Call the functions registered via setImmediate + JSTimersExecution.callImmediates(); + }); var currentOutgoingItems = this._outgoingItems; this._swapAndReinitializeBuffer(); From 219a7c1bfdf12bcd1ea38b74fc692a89069d14f1 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Mon, 1 Jun 2015 16:29:52 -0700 Subject: [PATCH 11/14] [ReactNative] Navigator block touches on scene when navigating --- Libraries/CustomComponents/Navigator/Navigator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index f75430b7790ecd..56bd98c73365a0 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -1290,7 +1290,7 @@ var Navigator = React.createClass({ key={this.state.idStack[i]} ref={'scene_' + i} onStartShouldSetResponderCapture={() => { - return i !== this.state.presentedIndex; + return !!this.state.transitionFromIndex || !!this.state.activeGesture; }} style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}> {React.cloneElement(child, { From e6c04df5a11628500a6538a546b3c549fda0cf99 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Mon, 1 Jun 2015 17:30:49 -0700 Subject: [PATCH 12/14] fix bug with inspector clicking Summary: Previously, if you were already inspecting an element, touching again would select a completely different element because the touch position was calculated relative to the current overlay. This fixes it. @public Test Plan: Open the inspector, click around, verify that every click selects the thing you clicked on. --- Libraries/ReactIOS/InspectorOverlay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/ReactIOS/InspectorOverlay.js b/Libraries/ReactIOS/InspectorOverlay.js index 8b5c6c0cb2ab8e..eeb6e7965728f9 100644 --- a/Libraries/ReactIOS/InspectorOverlay.js +++ b/Libraries/ReactIOS/InspectorOverlay.js @@ -59,7 +59,7 @@ var InspectorOverlay = React.createClass({ ? 'flex-start' : 'flex-end'; - content.push(); + content.push(); content.push(); } return ( From 1ed2542b464270f6da28f2255471b80ec36682a5 Mon Sep 17 00:00:00 2001 From: Chace Liang Date: Mon, 1 Jun 2015 20:17:25 -0700 Subject: [PATCH 13/14] Revert "[Bridge] Add support for JS async functions to RCT_EXPORT_METHOD" --- .../BatchedBridgeFactory.js | 68 ++++---------- Libraries/Utilities/MessageQueue.js | 14 +-- React/Base/RCTBridge.m | 89 +------------------ React/Base/RCTBridgeModule.h | 40 +-------- 4 files changed, 30 insertions(+), 181 deletions(-) diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js index 4702e246de9eed..dfc09ba7cbc219 100644 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js +++ b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js @@ -19,17 +19,9 @@ var slice = Array.prototype.slice; var MethodTypes = keyMirror({ remote: null, - remoteAsync: null, local: null, }); -type ErrorData = { - message: string; - domain: string; - code: number; - nativeStackIOS?: string; -}; - /** * Creates remotely invokable modules. */ @@ -44,40 +36,21 @@ var BatchedBridgeFactory = { */ _createBridgedModule: function(messageQueue, moduleConfig, moduleName) { var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) { - switch (methodConfig.type) { - case MethodTypes.remote: - return function() { - var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null; - var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; - var hasErrorCB = typeof lastArg === 'function'; - var hasSuccCB = typeof secondLastArg === 'function'; - hasSuccCB && invariant( - hasErrorCB, - 'Cannot have a non-function arg after a function arg.' - ); - var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0); - var args = slice.call(arguments, 0, arguments.length - numCBs); - var onSucc = hasSuccCB ? secondLastArg : null; - var onFail = hasErrorCB ? lastArg : null; - messageQueue.call(moduleName, memberName, args, onSucc, onFail); - }; - - case MethodTypes.remoteAsync: - return function(...args) { - return new Promise((resolve, reject) => { - messageQueue.call(moduleName, memberName, args, resolve, (errorData) => { - var error = _createErrorFromErrorData(errorData); - reject(error); - }); - }); - }; - - case MethodTypes.local: - return null; - - default: - throw new Error('Unknown bridge method type: ' + methodConfig.type); - } + return methodConfig.type === MethodTypes.local ? null : function() { + var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null; + var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; + var hasSuccCB = typeof lastArg === 'function'; + var hasErrorCB = typeof secondLastArg === 'function'; + hasErrorCB && invariant( + hasSuccCB, + 'Cannot have a non-function arg after a function arg.' + ); + var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0); + var args = slice.call(arguments, 0, arguments.length - numCBs); + var onSucc = hasSuccCB ? lastArg : null; + var onFail = hasErrorCB ? secondLastArg : null; + return messageQueue.call(moduleName, memberName, args, onFail, onSucc); + }; }); for (var constName in moduleConfig.constants) { warning(!remoteModule[constName], 'saw constant and method named %s', constName); @@ -86,6 +59,7 @@ var BatchedBridgeFactory = { return remoteModule; }, + create: function(MessageQueue, modulesConfig, localModulesConfig) { var messageQueue = new MessageQueue(modulesConfig, localModulesConfig); return { @@ -106,14 +80,4 @@ var BatchedBridgeFactory = { } }; -function _createErrorFromErrorData(errorData: ErrorData): Error { - var { - message, - ...extraErrorInfo, - } = errorData; - var error = new Error(message); - error.framesToPop = 1; - return Object.assign(error, extraErrorInfo); -} - module.exports = BatchedBridgeFactory; diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 2ff8b81d783be7..9f07cf4c0b6b94 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -431,14 +431,14 @@ var MessageQueueMixin = { }, /** - * @param {Function} onSucc Function to store in current thread for later - * lookup, when request succeeds. * @param {Function} onFail Function to store in current thread for later * lookup, when request fails. + * @param {Function} onSucc Function to store in current thread for later + * lookup, when request succeeds. * @param {Object?=} scope Scope to invoke `cb` with. * @param {Object?=} res Resulting callback ids. Use `this._POOLED_CBIDS`. */ - _storeCallbacksInCurrentThread: function(onSucc, onFail, scope) { + _storeCallbacksInCurrentThread: function(onFail, onSucc, scope) { invariant(onFail || onSucc, INTERNAL_ERROR); this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS); var succCBID = this._POOLED_CBIDS.successCallbackID; @@ -496,7 +496,7 @@ var MessageQueueMixin = { return ret; }, - call: function(moduleName, methodName, params, onSucc, onFail, scope) { + call: function(moduleName, methodName, params, onFail, onSucc, scope) { invariant( (!onFail || typeof onFail === 'function') && (!onSucc || typeof onSucc === 'function'), @@ -504,10 +504,10 @@ var MessageQueueMixin = { ); // Store callback _before_ sending the request, just in case the MailBox // returns the response in a blocking manner. - if (onSucc || onFail) { - this._storeCallbacksInCurrentThread(onSucc, onFail, scope, this._POOLED_CBIDS); - onSucc && params.push(this._POOLED_CBIDS.successCallbackID); + if (onSucc) { + this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS); onFail && params.push(this._POOLED_CBIDS.errorCallbackID); + params.push(this._POOLED_CBIDS.successCallbackID); } var moduleID = this._remoteModuleNameToModuleID[moduleName]; if (moduleID === undefined || moduleID === null) { diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index c1028a3fe700d2..47c7f59420c91d 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -47,11 +47,6 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; -typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { - RCTJavaScriptFunctionKindNormal, - RCTJavaScriptFunctionKindAsync, -}; - #ifdef __LP64__ typedef uint64_t RCTHeaderValue; typedef struct section_64 RCTHeaderSection; @@ -209,27 +204,6 @@ typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { return RCTModuleClassesByID; } -// TODO: Can we just replace RCTMakeError with this function instead? -static NSDictionary *RCTJSErrorFromNSError(NSError *error) -{ - NSString *errorMessage; - NSArray *stackTrace = [NSThread callStackSymbols]; - NSMutableDictionary *errorInfo = - [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; - - if (error) { - errorMessage = error.localizedDescription ?: @"Unknown error from a native module"; - errorInfo[@"domain"] = error.domain ?: RCTErrorDomain; - errorInfo[@"code"] = @(error.code); - } else { - errorMessage = @"Unknown error from a native module"; - errorInfo[@"domain"] = RCTErrorDomain; - errorInfo[@"code"] = @-1; - } - - return RCTMakeError(errorMessage, nil, errorInfo); -} - @class RCTBatchedBridge; @interface RCTBridge () @@ -265,7 +239,6 @@ @interface RCTModuleMethod : NSObject @property (nonatomic, copy, readonly) NSString *moduleClassName; @property (nonatomic, copy, readonly) NSString *JSMethodName; @property (nonatomic, assign, readonly) SEL selector; -@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind; @end @@ -447,50 +420,6 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { addBlockArgument(); useFallback = NO; - } else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) { - RCTAssert(i == numberOfArguments - 2, - @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", - _moduleClassName, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseResolveBlock value = (^(id result) { - NSArray *arguments = result ? @[result] : @[]; - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, arguments] - context:context]; - }); - ) - useFallback = NO; - _functionKind = RCTJavaScriptFunctionKindAsync; - } else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) { - RCTAssert(i == numberOfArguments - 1, - @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", - _moduleClassName, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { - NSDictionary *errorJSON = RCTJSErrorFromNSError(error); - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, @[errorJSON]] - context:context]; - }); - ) - useFallback = NO; - _functionKind = RCTJavaScriptFunctionKindAsync; } } @@ -569,18 +498,9 @@ - (void)invokeWithBridge:(RCTBridge *)bridge // Safety check if (arguments.count != _argumentBlocks.count) { - NSInteger actualCount = arguments.count; - NSInteger expectedCount = _argumentBlocks.count; - - // Subtract the implicit Promise resolver and rejecter functions for implementations of async functions - if (_functionKind == RCTJavaScriptFunctionKindAsync) { - actualCount -= 2; - expectedCount -= 2; - } - RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, - actualCount, expectedCount); + arguments.count, _argumentBlocks.count); return; } } @@ -605,8 +525,7 @@ - (void)invokeWithBridge:(RCTBridge *)bridge - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", - NSStringFromClass(self.class), self, _methodName, _JSMethodName]; + return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName]; } @end @@ -687,7 +606,7 @@ - (NSString *)description * }, * "methodName2": { * "methodID": 1, - * "type": "remoteAsync" + * "type": "remote" * }, * etc... * }, @@ -711,7 +630,7 @@ - (NSString *)description [methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) { methodsByName[method.JSMethodName] = @{ @"methodID": @(methodID), - @"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote", + @"type": @"remote", }; }]; diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 1528b8cd99d8ef..34b861ff3f8f10 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -17,20 +17,6 @@ */ typedef void (^RCTResponseSenderBlock)(NSArray *response); -/** - * Block that bridge modules use to resolve the JS promise waiting for a result. - * Nil results are supported and are converted to JS's undefined value. - */ -typedef void (^RCTPromiseResolveBlock)(id result); - -/** - * Block that bridge modules use to reject the JS promise waiting for a result. - * The error may be nil but it is preferable to pass an NSError object for more - * precise error messages. - */ -typedef void (^RCTPromiseRejectBlock)(NSError *error); - - /** * This constant can be returned from +methodQueue to force module * methods to be called on the JavaScript thread. This can have serious @@ -51,7 +37,7 @@ extern const dispatch_queue_t RCTJSThread; * A reference to the RCTBridge. Useful for modules that require access * to bridge features, such as sending events or making JS calls. This * will be set automatically by the bridge when it initializes the module. - * To implement this in your module, just add @synthesize bridge = _bridge; +* To implement this in your module, just add @synthesize bridge = _bridge; */ @property (nonatomic, weak) RCTBridge *bridge; @@ -84,26 +70,6 @@ extern const dispatch_queue_t RCTJSThread; * { ... } * * and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`. - * - * ## Promises - * - * Bridge modules can also define methods that are exported to JavaScript as - * methods that return a Promise, and are compatible with JS async functions. - * - * Declare the last two parameters of your native method to be a resolver block - * and a rejecter block. The resolver block must precede the rejecter block. - * - * For example: - * - * RCT_EXPORT_METHOD(doSomethingAsync:(NSString *)aString - * resolver:(RCTPromiseResolveBlock)resolve - * rejecter:(RCTPromiseRejectBlock)reject - * { ... } - * - * Calling `NativeModules.ModuleName.doSomethingAsync(aString)` from - * JavaScript will return a promise that is resolved or rejected when your - * native method implementation calls the respective block. - * */ #define RCT_EXPORT_METHOD(method) \ RCT_REMAP_METHOD(, method) @@ -152,7 +118,7 @@ extern const dispatch_queue_t RCTJSThread; RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername) /** - * Like RCT_EXTERN_MODULE, but allows setting a custom JavaScript name. + * Similar to RCT_EXTERN_MODULE but allows setting a custom JavaScript name */ #define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \ objc_name : objc_supername \ @@ -170,7 +136,7 @@ extern const dispatch_queue_t RCTJSThread; RCT_EXTERN_REMAP_METHOD(, method) /** - * Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name. + * Similar to RCT_EXTERN_REMAP_METHOD but allows setting a custom JavaScript name */ #define RCT_EXTERN_REMAP_METHOD(js_name, method) \ - (void)__rct_export__##method { \ From 570597c4ac0db0d75a1a51cfc8b07453d4d3e91d Mon Sep 17 00:00:00 2001 From: John Harper Date: Tue, 2 Jun 2015 01:38:50 -0700 Subject: [PATCH 14/14] [react-native] dispatch perf updates to main thread --- React/Base/RCTBridge.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 47c7f59420c91d..f122611cdc4d9d 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1555,7 +1555,9 @@ - (void)_jsThreadUpdate:(CADisplayLink *)displayLink RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); - [self.perfStats.jsGraph tick:displayLink.timestamp]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.perfStats.jsGraph tick:displayLink.timestamp]; + }); } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink