mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-24 04:16:00 +08:00
[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:
20
Libraries/BatchedBridge/BatchedBridge.js
Normal file
20
Libraries/BatchedBridge/BatchedBridge.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
142
Libraries/Utilities/__tests__/MessageQueue-test.js
Normal file
142
Libraries/Utilities/__tests__/MessageQueue-test.js
Normal 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 },
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user