[ReactNative] Refactor BatchedBridge and MessageQueue

Summary:
@public

The current implementation of `MessageQueue` is huge, over-complicated and spread
across `MethodQueue`, `MethodQueueMixin`, `BatchedBridge` and `BatchedBridgeFactory`

Refactored in a simpler way, were it's just a `MessageQueue` class and `BatchedBridge`
is only an instance of it.

Test Plan:
I had to make some updates to the tests, but no real update to the native side.
There's also tests covering the `remoteAsync` methods, and more integration tests for UIExplorer.
Verified whats being used by Android, and it should be safe, also tests Android tests have been pretty reliable.

Manually testing: Create a big hierarchy, like `<ListView>` example. Use the `TimerMixin` example to generate multiple calls.
Test the failure callback on the `Geolocation` example.

All the calls go through this entry point, so it's hard to miss if it's broken.
This commit is contained in:
Tadeu Zagallo
2015-06-17 07:51:48 -07:00
parent 6573d256ba
commit 92d98533f1
8 changed files with 394 additions and 693 deletions

View File

@@ -0,0 +1,20 @@
/**
* 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.
*
* @providesModule BatchedBridge
*/
'use strict';
let MessageQueue = require('MessageQueue');
let BatchedBridge = new MessageQueue(
__fbBatchedBridgeConfig.remoteModuleConfig,
__fbBatchedBridgeConfig.localModulesConfig,
);
module.exports = BatchedBridge;

View File

@@ -1,37 +0,0 @@
/**
* 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.
*
* @providesModule BatchedBridge
*/
'use strict';
var BatchedBridgeFactory = require('BatchedBridgeFactory');
var MessageQueue = require('MessageQueue');
/**
* Signature that matches the native IOS modules/methods that are exposed. We
* indicate which ones accept a callback. The order of modules and methods
* within them implicitly define their numerical *ID* that will be used to
* describe method calls across the wire. This is so that memory is used
* efficiently and we do not need to copy strings in native land - or across any
* wire.
*/
var remoteModulesConfig = __fbBatchedBridgeConfig.remoteModuleConfig;
var localModulesConfig = __fbBatchedBridgeConfig.localModulesConfig;
var BatchedBridge = BatchedBridgeFactory.create(
MessageQueue,
remoteModulesConfig,
localModulesConfig
);
BatchedBridge._config = remoteModulesConfig;
module.exports = BatchedBridge;

View File

@@ -1,116 +0,0 @@
/**
* 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.
*
* @providesModule BatchedBridgeFactory
*/
'use strict';
var invariant = require('invariant');
var keyMirror = require('keyMirror');
var mapObject = require('mapObject');
var warning = require('warning');
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.
*/
var BatchedBridgeFactory = {
MethodTypes: MethodTypes,
/**
* @param {MessageQueue} messageQueue Message queue that has been created with
* the `moduleConfig` (among others perhaps).
* @param {object} moduleConfig Configuration of module names/method
* names to callback types.
* @return {object} Remote representation of configured module.
*/
_createBridgedModule: function(messageQueue, moduleConfig, moduleName) {
var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) {
switch (methodConfig.type) {
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:
return 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);
remoteModule[constName] = moduleConfig.constants[constName];
}
return remoteModule;
},
create: function(MessageQueue, modulesConfig, localModulesConfig) {
var messageQueue = new MessageQueue(modulesConfig, localModulesConfig);
return {
callFunction: messageQueue.callFunction.bind(messageQueue),
callFunctionReturnFlushedQueue:
messageQueue.callFunctionReturnFlushedQueue.bind(messageQueue),
invokeCallback: messageQueue.invokeCallback.bind(messageQueue),
invokeCallbackAndReturnFlushedQueue:
messageQueue.invokeCallbackAndReturnFlushedQueue.bind(messageQueue),
flushedQueue: messageQueue.flushedQueue.bind(messageQueue),
RemoteModules: mapObject(modulesConfig, this._createBridgedModule.bind(this, messageQueue)),
setLoggingEnabled: messageQueue.setLoggingEnabled.bind(messageQueue),
getLoggedOutgoingItems: messageQueue.getLoggedOutgoingItems.bind(messageQueue),
getLoggedIncomingItems: messageQueue.getLoggedIncomingItems.bind(messageQueue),
replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue),
processBatch: messageQueue.processBatch.bind(messageQueue),
};
}
};
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;

View File

@@ -14,7 +14,7 @@
var GLOBAL = GLOBAL || this;
var BridgeProfiling = {
profile(profileName?: string, args?: any) {
profile(profileName?: any, args?: any) {
if (GLOBAL.__BridgeProfilingIsProfiling) {
if (args) {
try {
@@ -23,6 +23,8 @@ var BridgeProfiling = {
args = err.message;
}
}
profileName = typeof profileName === 'function' ?
profileName() : profileName;
console.profile(profileName, args);
}
},

View File

@@ -7,541 +7,241 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule MessageQueue
* @flow
*/
/*eslint no-bitwise: 0*/
'use strict';
var ErrorUtils = require('ErrorUtils');
var ReactUpdates = require('ReactUpdates');
let BridgeProfiling = require('BridgeProfiling');
let ErrorUtils = require('ErrorUtils');
let JSTimersExecution = require('JSTimersExecution');
let ReactUpdates = require('ReactUpdates');
var invariant = require('invariant');
var warning = require('warning');
let invariant = require('invariant');
let keyMirror = require('keyMirror');
let stringifySafe = require('stringifySafe');
var BridgeProfiling = require('BridgeProfiling');
var JSTimersExecution = require('JSTimersExecution');
let MODULE_IDS = 0;
let METHOD_IDS = 1;
let PARAMS = 2;
var INTERNAL_ERROR = 'Error in MessageQueue implementation';
let MethodTypes = keyMirror({
local: null,
remote: null,
remoteAsync: null,
});
// Prints all bridge traffic to console.log
var DEBUG_SPY_MODE = false;
type ModulesConfig = {
[key:string]: {
moduleID: number;
methods: {[key:string]: {
methodID: number;
}};
var guard = (fn) => {
try {
fn();
} catch (error) {
ErrorUtils.reportFatalError(error);
}
};
class MessageQueue {
constructor(remoteModules, localModules, customRequire) {
this.RemoteModules = {};
this._require = customRequire || require;
this._queue = [[],[],[]];
this._moduleTable = {};
this._methodTable = {};
this._callbacks = [];
this._callbackID = 0;
[
'processBatch',
'invokeCallbackAndReturnFlushedQueue',
'callFunctionReturnFlushedQueue',
'flushedQueue',
].forEach((fn) => this[fn] = this[fn].bind(this));
this._genModules(remoteModules);
localModules && this._genLookupTables(
localModules, this._moduleTable, this._methodTable);
if (__DEV__) {
this._debugInfo = {};
this._remoteModuleTable = {};
this._remoteMethodTable = {};
this._genLookupTables(
remoteModules, this._remoteModuleTable, this._remoteMethodTable);
}
}
/**
* Public APIs
*/
processBatch(batch) {
ReactUpdates.batchedUpdates(() => {
batch.forEach((call) => {
let method = call.method === 'callFunctionReturnFlushedQueue' ?
'__callFunction' : '__invokeCallback';
guard(() => this[method].apply(this, call.args));
});
BridgeProfiling.profile('ReactUpdates.batchedUpdates()');
});
BridgeProfiling.profileEnd();
return this.flushedQueue();
}
callFunctionReturnFlushedQueue(module, method, args) {
guard(() => this.__callFunction(module, method, args));
return this.flushedQueue();
}
invokeCallbackAndReturnFlushedQueue(cbID, args) {
guard(() => this.__invokeCallback(cbID, args));
return this.flushedQueue();
}
flushedQueue() {
BridgeProfiling.profile('JSTimersExecution.callImmediates()');
guard(() => JSTimersExecution.callImmediates());
BridgeProfiling.profileEnd();
let queue = this._queue;
this._queue = [[],[],[]];
return queue[0].length ? queue : null;
}
/**
* "Private" methods
*/
__nativeCall(module, method, params, onFail, onSucc) {
if (onFail || onSucc) {
if (__DEV__) {
// eventually delete old debug info
(this._callbackID > (1 << 5)) &&
(this._debugInfo[this._callbackID >> 5] = null);
this._debugInfo[this._callbackID >> 1] = [module, method];
}
onFail && params.push(this._callbackID);
this._callbacks[this._callbackID++] = onFail;
onSucc && params.push(this._callbackID);
this._callbacks[this._callbackID++] = onSucc;
}
this._queue[MODULE_IDS].push(module);
this._queue[METHOD_IDS].push(method);
this._queue[PARAMS].push(params);
}
__callFunction(module, method, args) {
BridgeProfiling.profile(() => `${module}.${method}(${stringifySafe(args)})`);
if (isFinite(module)) {
method = this._methodTable[module][method];
module = this._moduleTable[module];
}
module = this._require(module);
module[method].apply(module, args);
BridgeProfiling.profileEnd();
}
__invokeCallback(cbID, args) {
BridgeProfiling.profile(
() => `MessageQueue.invokeCallback(${cbID}, ${stringifySafe(args)})`);
let callback = this._callbacks[cbID];
if (__DEV__ && !callback) {
let debug = this._debugInfo[cbID >> 1];
let module = this._remoteModuleTable[debug[0]];
let method = this._remoteMethodTable[debug[0]][debug[1]];
console.error(`Callback with id ${cbID}: ${module}.${method}() not found`);
}
this._callbacks[cbID & ~1] = null;
this._callbacks[cbID | 1] = null;
callback.apply(null, args);
BridgeProfiling.profileEnd();
}
/**
* Private helper methods
*/
_genLookupTables(localModules, moduleTable, methodTable) {
let moduleNames = Object.keys(localModules);
for (var i = 0, l = moduleNames.length; i < l; i++) {
let moduleName = moduleNames[i];
let methods = localModules[moduleName].methods;
let moduleID = localModules[moduleName].moduleID;
moduleTable[moduleID] = moduleName;
methodTable[moduleID] = {};
let methodNames = Object.keys(methods);
for (var j = 0, k = methodNames.length; j < k; j++) {
let methodName = methodNames[j];
let methodConfig = methods[methodName];
methodTable[moduleID][methodConfig.methodID] = methodName;
}
}
}
_genModules(remoteModules) {
let moduleNames = Object.keys(remoteModules);
for (var i = 0, l = moduleNames.length; i < l; i++) {
let moduleName = moduleNames[i];
let moduleConfig = remoteModules[moduleName];
this.RemoteModules[moduleName] = this._genModule({}, moduleConfig);
}
}
_genModule(module, moduleConfig) {
let methodNames = Object.keys(moduleConfig.methods);
for (var i = 0, l = methodNames.length; i < l; i++) {
let methodName = methodNames[i];
let methodConfig = moduleConfig.methods[methodName];
module[methodName] = this._genMethod(
moduleConfig.moduleID, methodConfig.methodID, methodConfig.type);
}
Object.assign(module, moduleConfig.constants);
return module;
}
_genMethod(module, method, type) {
if (type === MethodTypes.local) {
return null;
}
let self = this;
if (type === MethodTypes.remoteAsync) {
return function(...args) {
return new Promise((resolve, reject) => {
self.__nativeCall(module, method, args, resolve, (errorData) => {
var error = createErrorFromErrorData(errorData);
reject(error);
});
});
};
} else {
return function(...args) {
let lastArg = args.length > 0 ? args[args.length - 1] : null;
let secondLastArg = args.length > 1 ? args[args.length - 2] : null;
let hasSuccCB = typeof lastArg === 'function';
let hasErrorCB = typeof secondLastArg === 'function';
hasErrorCB && invariant(
hasSuccCB,
'Cannot have a non-function arg after a function arg.'
);
let numCBs = hasSuccCB + hasErrorCB;
let onSucc = hasSuccCB ? lastArg : null;
let onFail = hasErrorCB ? secondLastArg : null;
args = args.slice(0, args.length - numCBs);
return self.__nativeCall(module, method, args, onFail, onSucc);
};
}
}
}
type NameToID = {[key:string]: number}
type IDToName = {[key:number]: string}
function createErrorFromErrorData(errorData: ErrorData): Error {
var {
message,
...extraErrorInfo,
} = errorData;
var error = new Error(message);
error.framesToPop = 1;
return Object.assign(error, extraErrorInfo);
}
/**
* So as not to confuse static build system.
*/
var requireFunc = require;
/**
* @param {Object!} module Module instance, must be loaded.
* @param {string} methodName Name of method in `module`.
* @param {array<*>} params Arguments to method.
* @returns {*} Return value of method invocation.
*/
var jsCall = function(module, methodName, params) {
return module[methodName].apply(module, params);
};
/**
* A utility for aggregating "work" to be done, and potentially transferring
* that work to another thread. Each instance of `MessageQueue` has the notion
* of a "target" thread - the thread that the work will be sent to.
*
* TODO: Long running callback results, and streaming callback results (ability
* for a callback to be invoked multiple times).
*
* @param {object} moduleNameToID Used to translate module/method names into
* efficient numeric IDs.
* @class MessageQueue
*/
var MessageQueue = function(
remoteModulesConfig: ModulesConfig,
localModulesConfig: ModulesConfig,
customRequire: (id: string) => any
) {
this._requireFunc = customRequire || requireFunc;
this._initBookeeping();
this._initNamingMap(remoteModulesConfig, localModulesConfig);
};
// REQUEST: Parallell arrays:
var REQUEST_MODULE_IDS = 0;
var REQUEST_METHOD_IDS = 1;
var REQUEST_PARAMSS = 2;
// RESPONSE: Parallell arrays:
var RESPONSE_CBIDS = 3;
var RESPONSE_RETURN_VALUES = 4;
var applyWithErrorReporter = function(fun: Function, context: ?any, args: ?any) {
try {
return fun.apply(context, args);
} catch (e) {
ErrorUtils.reportFatalError(e);
}
};
/**
* Utility to catch errors and prevent having to bind, or execute a bound
* function, while catching errors in a process and returning a resulting
* return value. This ensures that even if a process fails, we can still return
* *some* values (from `_flushedQueueUnguarded` for example). Glorified
* try/catch/finally that invokes the global `onerror`.
*
* @param {function} operation Function to execute, likely populates the
* message buffer.
* @param {Array<*>} operationArguments Arguments passed to `operation`.
* @param {function} getReturnValue Returns a return value - will be invoked
* even if the `operation` fails half way through completing its task.
* @return {object} Return value returned from `getReturnValue`.
*/
var guardReturn = function(operation, operationArguments, getReturnValue, context) {
if (operation) {
applyWithErrorReporter(operation, context, operationArguments);
}
if (getReturnValue) {
return applyWithErrorReporter(getReturnValue, context, null);
}
return null;
};
/**
* Bookkeeping logic for callbackIDs. We ensure that success and error
* callbacks are numerically adjacent.
*
* We could have also stored the association between success cbID and errorCBID
* in a map without relying on this adjacency, but the bookkeeping here avoids
* an additional two maps to associate in each direction, and avoids growing
* dictionaries (new fields). Instead, we compute pairs of callback IDs, by
* populating the `res` argument to `allocateCallbackIDs` (in conjunction with
* pooling). Behind this bookeeping API, we ensure that error and success
* callback IDs are always adjacent so that when one is invoked, we always know
* how to free the memory of the other. By using this API, it is impossible to
* create malformed callbackIDs that are not adjacent.
*/
var createBookkeeping = function() {
return {
/**
* Incrementing callback ID. Must start at 1 - otherwise converted null
* values which become zero are not distinguishable from a GUID of zero.
*/
GUID: 1,
errorCallbackIDForSuccessCallbackID: function(successID) {
return successID + 1;
},
successCallbackIDForErrorCallbackID: function(errorID) {
return errorID - 1;
},
allocateCallbackIDs: function(res) {
res.successCallbackID = this.GUID++;
res.errorCallbackID = this.GUID++;
},
isSuccessCallback: function(id) {
return id % 2 === 1;
}
};
};
var MessageQueueMixin = {
/**
* Creates an efficient wire protocol for communicating across a bridge.
* Avoids allocating strings.
*
* @param {object} remoteModulesConfig Configuration of modules and their
* methods.
*/
_initNamingMap: function(
remoteModulesConfig: ModulesConfig,
localModulesConfig: ModulesConfig
) {
this._remoteModuleNameToModuleID = {};
this._remoteModuleIDToModuleName = {}; // Reverse
this._remoteModuleNameToMethodNameToID = {};
this._remoteModuleNameToMethodIDToName = {}; // Reverse
this._localModuleNameToModuleID = {};
this._localModuleIDToModuleName = {}; // Reverse
this._localModuleNameToMethodNameToID = {};
this._localModuleNameToMethodIDToName = {}; // Reverse
function fillMappings(
modulesConfig: ModulesConfig,
moduleNameToModuleID: NameToID,
moduleIDToModuleName: IDToName,
moduleNameToMethodNameToID: {[key:string]: NameToID},
moduleNameToMethodIDToName: {[key:string]: IDToName}
) {
for (var moduleName in modulesConfig) {
var moduleConfig = modulesConfig[moduleName];
var moduleID = moduleConfig.moduleID;
moduleNameToModuleID[moduleName] = moduleID;
moduleIDToModuleName[moduleID] = moduleName; // Reverse
moduleNameToMethodNameToID[moduleName] = {};
moduleNameToMethodIDToName[moduleName] = {}; // Reverse
var methods = moduleConfig.methods;
for (var methodName in methods) {
var methodID = methods[methodName].methodID;
moduleNameToMethodNameToID[moduleName][methodName] =
methodID;
moduleNameToMethodIDToName[moduleName][methodID] =
methodName; // Reverse
}
}
}
fillMappings(
remoteModulesConfig,
this._remoteModuleNameToModuleID,
this._remoteModuleIDToModuleName,
this._remoteModuleNameToMethodNameToID,
this._remoteModuleNameToMethodIDToName
);
fillMappings(
localModulesConfig,
this._localModuleNameToModuleID,
this._localModuleIDToModuleName,
this._localModuleNameToMethodNameToID,
this._localModuleNameToMethodIDToName
);
},
_initBookeeping: function() {
this._POOLED_CBIDS = {errorCallbackID: null, successCallbackID: null};
this._bookkeeping = createBookkeeping();
/**
* Stores callbacks so that we may simulate asynchronous return values from
* other threads. Remote invocations in other threads can pass return values
* back asynchronously to the requesting thread.
*/
this._threadLocalCallbacksByID = [];
this._threadLocalScopesByID = [];
/**
* Memory efficient parallel arrays. Each index cuts through the three
* arrays and forms a remote invocation of methodName(params) whos return
* value will be reported back to the other thread by way of the
* corresponding id in cbIDs. Each entry (A-D in the graphic below),
* represents a work item of the following form:
* - moduleID: ID of module to invoke method from.
* - methodID: ID of method in module to invoke.
* - params: List of params to pass to method.
* - cbID: ID to respond back to originating thread with.
*
* TODO: We can make this even more efficient (memory) by creating a single
* array, that is always pushed `n` elements as a time.
*/
this._outgoingItems = [
/*REQUEST_MODULE_IDS: */ [/* +-+ +-+ +-+ +-+ */],
/*REQUEST_METHOD_IDS: */ [/* |A| |B| |C| |D| */],
/*REQUEST_PARAMSS: */ [/* |-| |-| |-| |-| */],
/*RESPONSE_CBIDS: */ [/* +-+ +-+ +-+ +-+ */],
/* |E| |F| |G| |H| */
/*RESPONSE_RETURN_VALUES: */ [/* +-+ +-+ +-+ +-+ */]
];
/**
* Used to allow returning the buffer, while at the same time clearing it in
* a memory efficient manner.
*/
this._outgoingItemsSwap = [[], [], [], [], []];
},
invokeCallback: function(cbID, args) {
return guardReturn(this._invokeCallback, [cbID, args], null, this);
},
_invokeCallback: function(cbID, args) {
try {
var cb = this._threadLocalCallbacksByID[cbID];
var scope = this._threadLocalScopesByID[cbID];
warning(
cb,
'Cannot find callback with CBID %s. Native module may have invoked ' +
'both the success callback and the error callback.',
cbID
);
if (DEBUG_SPY_MODE) {
console.log('N->JS: Callback#' + cbID + '(' + JSON.stringify(args) + ')');
}
BridgeProfiling.profile('Callback#' + cbID + '(' + JSON.stringify(args) + ')');
cb.apply(scope, args);
BridgeProfiling.profileEnd();
} catch(ie_requires_catch) {
throw ie_requires_catch;
} finally {
// Clear out the memory regardless of success or failure.
this._freeResourcesForCallbackID(cbID);
}
},
invokeCallbackAndReturnFlushedQueue: function(cbID, args) {
if (this._enableLogging) {
this._loggedIncomingItems.push([new Date().getTime(), cbID, args]);
}
return guardReturn(
this._invokeCallback,
[cbID, args],
this._flushedQueueUnguarded,
this
);
},
callFunction: function(moduleID, methodID, params) {
return guardReturn(this._callFunction, [moduleID, methodID, params], null, this);
},
_callFunction: function(moduleName, methodName, params) {
if (isFinite(moduleName)) {
moduleName = this._localModuleIDToModuleName[moduleName];
methodName = this._localModuleNameToMethodIDToName[moduleName][methodName];
}
if (DEBUG_SPY_MODE) {
console.log(
'N->JS: ' + moduleName + '.' + methodName +
'(' + JSON.stringify(params) + ')');
}
BridgeProfiling.profile(moduleName + '.' + methodName + '(' + JSON.stringify(params) + ')');
var ret = jsCall(this._requireFunc(moduleName), methodName, params);
BridgeProfiling.profileEnd();
return ret;
},
callFunctionReturnFlushedQueue: function(moduleID, methodID, params) {
if (this._enableLogging) {
this._loggedIncomingItems.push([new Date().getTime(), moduleID, methodID, params]);
}
return guardReturn(
this._callFunction,
[moduleID, methodID, params],
this._flushedQueueUnguarded,
this
);
},
processBatch: function(batch) {
var self = this;
BridgeProfiling.profile('MessageQueue.processBatch()');
var flushedQueue = guardReturn(function () {
ReactUpdates.batchedUpdates(function() {
batch.forEach(function(call) {
invariant(
call.module === 'BatchedBridge',
'All the calls should pass through the BatchedBridge module'
);
if (call.method === 'callFunctionReturnFlushedQueue') {
self._callFunction.apply(self, call.args);
} else if (call.method === 'invokeCallbackAndReturnFlushedQueue') {
self._invokeCallback.apply(self, call.args);
} else {
throw new Error(
'Unrecognized method called on BatchedBridge: ' + call.method);
}
});
BridgeProfiling.profile('React.batchedUpdates()');
});
BridgeProfiling.profileEnd();
}, null, this._flushedQueueUnguarded, this);
BridgeProfiling.profileEnd();
return flushedQueue;
},
setLoggingEnabled: function(enabled) {
this._enableLogging = enabled;
this._loggedIncomingItems = [];
this._loggedOutgoingItems = [[], [], [], [], []];
},
getLoggedIncomingItems: function() {
return this._loggedIncomingItems;
},
getLoggedOutgoingItems: function() {
return this._loggedOutgoingItems;
},
replayPreviousLog: function(previousLog) {
this._outgoingItems = previousLog;
},
/**
* Simple helpers for clearing the queues. This doesn't handle the fact that
* memory in the current buffer is leaked until the next frame or update - but
* that will typically be on the order of < 500ms.
*/
_swapAndReinitializeBuffer: function() {
// Outgoing requests
var currentOutgoingItems = this._outgoingItems;
var nextOutgoingItems = this._outgoingItemsSwap;
nextOutgoingItems[REQUEST_MODULE_IDS].length = 0;
nextOutgoingItems[REQUEST_METHOD_IDS].length = 0;
nextOutgoingItems[REQUEST_PARAMSS].length = 0;
// Outgoing responses
nextOutgoingItems[RESPONSE_CBIDS].length = 0;
nextOutgoingItems[RESPONSE_RETURN_VALUES].length = 0;
this._outgoingItemsSwap = currentOutgoingItems;
this._outgoingItems = nextOutgoingItems;
},
/**
* @param {string} moduleID JS module name.
* @param {methodName} methodName Method in module to invoke.
* @param {array<*>?} params Array representing arguments to method.
* @param {string} cbID Unique ID to pass back in potential response.
*/
_pushRequestToOutgoingItems: function(moduleID, methodName, params) {
this._outgoingItems[REQUEST_MODULE_IDS].push(moduleID);
this._outgoingItems[REQUEST_METHOD_IDS].push(methodName);
this._outgoingItems[REQUEST_PARAMSS].push(params);
if (this._enableLogging) {
this._loggedOutgoingItems[REQUEST_MODULE_IDS].push(moduleID);
this._loggedOutgoingItems[REQUEST_METHOD_IDS].push(methodName);
this._loggedOutgoingItems[REQUEST_PARAMSS].push(params);
}
},
/**
* @param {string} cbID Unique ID that other side of bridge has remembered.
* @param {*} returnValue Return value to pass to callback on other side of
* bridge.
*/
_pushResponseToOutgoingItems: function(cbID, returnValue) {
this._outgoingItems[RESPONSE_CBIDS].push(cbID);
this._outgoingItems[RESPONSE_RETURN_VALUES].push(returnValue);
},
_freeResourcesForCallbackID: function(cbID) {
var correspondingCBID = this._bookkeeping.isSuccessCallback(cbID) ?
this._bookkeeping.errorCallbackIDForSuccessCallbackID(cbID) :
this._bookkeeping.successCallbackIDForErrorCallbackID(cbID);
this._threadLocalCallbacksByID[cbID] = null;
this._threadLocalScopesByID[cbID] = null;
if (this._threadLocalCallbacksByID[correspondingCBID]) {
this._threadLocalCallbacksByID[correspondingCBID] = null;
this._threadLocalScopesByID[correspondingCBID] = null;
}
},
/**
* @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(onFail, onSucc, scope) {
invariant(onFail || onSucc, INTERNAL_ERROR);
this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS);
var succCBID = this._POOLED_CBIDS.successCallbackID;
var errorCBID = this._POOLED_CBIDS.errorCallbackID;
this._threadLocalCallbacksByID[errorCBID] = onFail;
this._threadLocalCallbacksByID[succCBID] = onSucc;
this._threadLocalScopesByID[errorCBID] = scope;
this._threadLocalScopesByID[succCBID] = scope;
},
/**
* IMPORTANT: There is possibly a timing issue with this form of flushing. We
* are currently not seeing any problems but the potential issue to look out
* for is:
* - While flushing this._outgoingItems contains the work for the other thread
* to perform.
* - To mitigate this, we never allow enqueueing messages if the queue is
* already reserved - as long as it is reserved, it could be in the midst of
* a flush.
*
* If this ever occurs we can easily eliminate the race condition. We can
* completely solve any ambiguity by sending messages such that we'll never
* try to reserve the queue when already reserved. Here's the pseudocode:
*
* var defensiveCopy = efficientDefensiveCopy(this._outgoingItems);
* this._swapAndReinitializeBuffer();
*/
flushedQueue: function() {
return guardReturn(null, null, this._flushedQueueUnguarded, this);
},
_flushedQueueUnguarded: function() {
BridgeProfiling.profile('JSTimersExecution.callImmediates()');
// Call the functions registered via setImmediate
JSTimersExecution.callImmediates();
BridgeProfiling.profileEnd();
var currentOutgoingItems = this._outgoingItems;
this._swapAndReinitializeBuffer();
var ret = currentOutgoingItems[REQUEST_MODULE_IDS].length ||
currentOutgoingItems[RESPONSE_RETURN_VALUES].length ? currentOutgoingItems : null;
if (DEBUG_SPY_MODE && ret) {
for (var i = 0; i < currentOutgoingItems[0].length; i++) {
var moduleName = this._remoteModuleIDToModuleName[currentOutgoingItems[0][i]];
var methodName =
this._remoteModuleNameToMethodIDToName[moduleName][currentOutgoingItems[1][i]];
console.log(
'JS->N: ' + moduleName + '.' + methodName +
'(' + JSON.stringify(currentOutgoingItems[2][i]) + ')');
}
}
return ret;
},
call: function(moduleName, methodName, params, onFail, onSucc, 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 (onFail || onSucc) {
this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS);
onFail && params.push(this._POOLED_CBIDS.errorCallbackID);
onSucc && params.push(this._POOLED_CBIDS.successCallbackID);
}
var moduleID = this._remoteModuleNameToModuleID[moduleName];
if (moduleID === undefined || moduleID === null) {
throw new Error('Unrecognized module name:' + moduleName);
}
var methodID = this._remoteModuleNameToMethodNameToID[moduleName][methodName];
if (methodID === undefined || moduleID === null) {
throw new Error('Unrecognized method name:' + methodName);
}
this._pushRequestToOutgoingItems(moduleID, methodID, params);
},
__numPendingCallbacksOnlyUseMeInTestCases: function() {
var callbacks = this._threadLocalCallbacksByID;
var total = 0;
for (var i = 0; i < callbacks.length; i++) {
if (callbacks[i]) {
total++;
}
}
return total;
}
};
Object.assign(MessageQueue.prototype, MessageQueueMixin);
module.exports = MessageQueue;

View File

@@ -0,0 +1,142 @@
/**
* 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.
*/
'use strict';
jest.dontMock('MessageQueue');
var ReactUpdates = require('ReactUpdates');
var MessageQueue = require('MessageQueue');
let MODULE_IDS = 0;
let METHOD_IDS = 1;
let PARAMS = 2;
let TestModule = {
testHook1(){}, testHook2(){},
};
let customRequire = (moduleName) => TestModule;
let assertQueue = (flushedQueue, index, moduleID, methodID, params) => {
expect(flushedQueue[MODULE_IDS][index]).toEqual(moduleID);
expect(flushedQueue[METHOD_IDS][index]).toEqual(methodID);
expect(flushedQueue[PARAMS][index]).toEqual(params);
};
var queue;
describe('MessageQueue', () => {
beforeEach(() => {
queue = new MessageQueue(
remoteModulesConfig,
localModulesConfig,
customRequire,
);
TestModule.testHook1 = jasmine.createSpy();
TestModule.testHook2 = jasmine.createSpy();
});
it('should enqueue native calls', () => {
queue.__nativeCall(0, 1, [2]);
let flushedQueue = queue.flushedQueue();
assertQueue(flushedQueue, 0, 0, 1, [2]);
});
it('should call a local function with id', () => {
expect(TestModule.testHook1.callCount).toEqual(0);
queue.__callFunction(0, 0, [1]);
expect(TestModule.testHook1.callCount).toEqual(1);
});
it('should call a local function with the function name', () => {
expect(TestModule.testHook2.callCount).toEqual(0);
queue.__callFunction('one', 'testHook2', [2]);
expect(TestModule.testHook2.callCount).toEqual(1);
});
it('should generate native modules', () => {
queue.RemoteModules.one.remoteMethod1('foo');
let flushedQueue = queue.flushedQueue();
assertQueue(flushedQueue, 0, 0, 0, ['foo']);
});
it('should store callbacks', () => {
queue.RemoteModules.one.remoteMethod2('foo', () => {}, () => {});
let flushedQueue = queue.flushedQueue();
assertQueue(flushedQueue, 0, 0, 1, ['foo', 0, 1]);
});
it('should call the stored callback', (done) => {
var done = false;
queue.RemoteModules.one.remoteMethod1(() => { done = true; });
queue.__invokeCallback(1);
expect(done).toEqual(true);
});
it('should throw when calling the same callback twice', () => {
queue.RemoteModules.one.remoteMethod1(() => {});
queue.__invokeCallback(1);
expect(() => queue.__invokeCallback(1)).toThrow();
});
it('should throw when calling both success and failure callback', () => {
queue.RemoteModules.one.remoteMethod1(() => {}, () => {});
queue.__invokeCallback(1);
expect(() => queue.__invokeCallback(0)).toThrow();
});
describe('processBatch', () => {
beforeEach(() => {
ReactUpdates.batchedUpdates = (fn) => fn();
});
it('should call __invokeCallback for invokeCallbackAndReturnFlushedQueue', () => {
queue.__invokeCallback = jasmine.createSpy();
queue.processBatch([{
method: 'invokeCallbackAndReturnFlushedQueue',
args: [],
}]);
expect(queue.__invokeCallback.callCount).toEqual(1);
});
it('should call __callFunction for callFunctionReturnFlushedQueue', () => {
queue.__callFunction = jasmine.createSpy();
queue.processBatch([{
method: 'callFunctionReturnFlushedQueue',
args: [],
}]);
expect(queue.__callFunction.callCount).toEqual(1);
});
});
});
var remoteModulesConfig = {
'one': {
'moduleID':0,
'methods': {
'remoteMethod1':{ 'type': 'remote', 'methodID': 0 },
'remoteMethod2':{ 'type': 'remote', 'methodID': 1 },
}
},
};
var localModulesConfig = {
'one': {
'moduleID': 0,
'methods': {
'testHook1':{ 'type': 'local', 'methodID': 0 },
'testHook2':{ 'type': 'local', 'methodID': 1 },
}
},
};