Skip to content

Commit

Permalink
[RFC][Bridge] Add support for JS async functions, backed by RCT_EXPOR…
Browse files Browse the repository at this point in the history
…T_PROMISE

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_PROMISE(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 is
  // available under nativeStackIOS
});
```

And if you take a time machine or use Babel w/stage 1, you can write:

```js
try {
  var value = await DemoDataStore.getValueAsync('sample-key');
  console.log('Got:', value);
} catch (error) {
  console.error(error);
}
```

For now the macro is defined as RCT_EXPORT_PROMISE_EXPERIMENTAL since I'd like to merge this and get real-world feedback on the API before committing to it.
  • Loading branch information
ide committed May 12, 2015
1 parent 41612f3 commit 7d63535
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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);
Expand All @@ -59,7 +86,6 @@ var BatchedBridgeFactory = {
return remoteModule;
},


create: function(MessageQueue, modulesConfig, localModulesConfig) {
var messageQueue = new MessageQueue(modulesConfig, localModulesConfig);
return {
Expand All @@ -80,4 +106,13 @@ var BatchedBridgeFactory = {
}
};

function _createErrorFromErrorData(errorData: ErrorData): Error {
var {
message,
...extraErrorInfo,
} = errorData;
var error = new Error(message);
return Object.assign(error, extraErrorInfo);
}

module.exports = BatchedBridgeFactory;
14 changes: 7 additions & 7 deletions Libraries/Utilities/MessageQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,14 +423,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;
Expand Down Expand Up @@ -486,18 +486,18 @@ 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'),
'Callbacks must be functions'
);
// 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) {
Expand Down
84 changes: 78 additions & 6 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import <mach-o/dyld.h>
#import <mach-o/getsect.h>

#import "RCTBridgeModule_Internal.h"
#import "RCTContextExecutor.h"
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
Expand Down Expand Up @@ -240,6 +241,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) RCTBridgeFunctionKind functionKind;

@end

Expand All @@ -266,8 +268,10 @@ @implementation RCTModuleMethod
- (instancetype)initWithReactMethodName:(NSString *)reactMethodName
objCMethodName:(NSString *)objCMethodName
JSMethodName:(NSString *)JSMethodName
functionKind:(RCTBridgeFunctionKind)functionKind
{
if ((self = [super init])) {
_functionKind = functionKind;
_methodName = reactMethodName;
NSArray *parts = [[reactMethodName substringWithRange:(NSRange){2, reactMethodName.length - 3}] componentsSeparatedByString:@" "];

Expand Down Expand Up @@ -421,6 +425,43 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
} else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) {
addBlockArgument();
useFallback = NO;
} else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) {
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;
} else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) {
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 RCTPromiseRejectBlock value = (^(NSError *error) {
NSDictionary *errorData = [RCTModuleMethod dictionaryFromError:error
stackTrace:[NSThread callStackSymbols]];
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, @[errorData]]
context:context];
});
)
useFallback = NO;
}
}

Expand Down Expand Up @@ -499,9 +540,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 == RCTBridgeFunctionKindAsync) {
actualCount -= 2;
expectedCount -= 2;
}

RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd",
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
arguments.count, _argumentBlocks.count);
actualCount, expectedCount);
return;
}
}
Expand Down Expand Up @@ -529,6 +579,26 @@ - (NSString *)description
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName];
}

+ (NSDictionary *)dictionaryFromError:(NSError *)error stackTrace:(NSArray *)stackTrace
{
NSString *errorMessage;
NSMutableDictionary *errorInfo = [NSMutableDictionary dictionaryWithDictionary:@{
@"nativeStackIOS": stackTrace,
}];

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);
}

@end

/**
Expand Down Expand Up @@ -557,7 +627,7 @@ - (NSString *)description

for (RCTHeaderValue addr = section->offset;
addr < section->offset + section->size;
addr += sizeof(const char **) * 3) {
addr += sizeof(const char **) * 4) {

// Get data entry
const char **entries = (const char **)(mach_header + addr);
Expand All @@ -569,11 +639,13 @@ - (NSString *)description
// Legacy support for RCT_EXPORT()
moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0])
objCMethodName:@(entries[0])
JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil];
JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil
functionKind:(RCTBridgeFunctionKind)entries[3]];
} else {
moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0])
objCMethodName:strlen(entries[1]) ? @(entries[1]) : nil
JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil];
JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil
functionKind:(RCTBridgeFunctionKind)entries[3]];
}

// Cache method
Expand Down Expand Up @@ -607,7 +679,7 @@ - (NSString *)description
* },
* "methodName2": {
* "methodID": 1,
* "type": "remote"
* "type": "remoteAsync"
* },
* etc...
* },
Expand All @@ -631,7 +703,7 @@ - (NSString *)description
[methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) {
methodsByName[method.JSMethodName] = @{
@"methodID": @(methodID),
@"type": @"remote",
@"type": method.functionKind == RCTBridgeFunctionKindAsync ? @"remoteAsync" : @"remote",
};
}];

Expand Down
Loading

0 comments on commit 7d63535

Please sign in to comment.