Extract native module logic from BatchedBridge
Reviewed By: lexs Differential Revision: D3901630 fbshipit-source-id: c119ffe54a4d1e716e6ae98895e5a3a48b16cf43
This commit is contained in:
parent
31b158c9fe
commit
76c54847bb
|
@ -7,20 +7,16 @@
|
|||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule BatchedBridge
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const MessageQueue = require('MessageQueue');
|
||||
|
||||
const BatchedBridge = new MessageQueue(() => global.__fbBatchedBridgeConfig);
|
||||
const BatchedBridge = new MessageQueue();
|
||||
|
||||
// TODO: Move these around to solve the cycle in a cleaner way.
|
||||
|
||||
const Systrace = require('Systrace');
|
||||
const JSTimersExecution = require('JSTimersExecution');
|
||||
|
||||
BatchedBridge.registerCallableModule('Systrace', Systrace);
|
||||
BatchedBridge.registerCallableModule('JSTimersExecution', JSTimersExecution);
|
||||
BatchedBridge.registerCallableModule('Systrace', require('Systrace'));
|
||||
BatchedBridge.registerCallableModule('JSTimersExecution', require('JSTimersExecution'));
|
||||
BatchedBridge.registerCallableModule('HeapCapture', require('HeapCapture'));
|
||||
BatchedBridge.registerCallableModule('SamplingProfiler', require('SamplingProfiler'));
|
||||
|
||||
|
|
|
@ -12,32 +12,130 @@
|
|||
'use strict';
|
||||
|
||||
const BatchedBridge = require('BatchedBridge');
|
||||
const RemoteModules = BatchedBridge.RemoteModules;
|
||||
|
||||
/**
|
||||
* Define lazy getters for each module.
|
||||
* These will return the module if already loaded, or load it if not.
|
||||
*/
|
||||
const NativeModules = {};
|
||||
Object.keys(RemoteModules).forEach((moduleName) => {
|
||||
Object.defineProperty(NativeModules, moduleName, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
let module = RemoteModules[moduleName];
|
||||
if (module && typeof module.moduleID === 'number' && global.nativeRequireModuleConfig) {
|
||||
const config = global.nativeRequireModuleConfig(moduleName);
|
||||
module = config && BatchedBridge.processModuleConfig(config, module.moduleID);
|
||||
RemoteModules[moduleName] = module;
|
||||
}
|
||||
Object.defineProperty(NativeModules, moduleName, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: module,
|
||||
});
|
||||
return module;
|
||||
},
|
||||
const defineLazyObjectProperty = require('defineLazyObjectProperty');
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
type ModuleConfig = [
|
||||
string, /* name */
|
||||
?Object, /* constants */
|
||||
Array<string>, /* functions */
|
||||
Array<number>, /* promise method IDs */
|
||||
Array<number>, /* sync method IDs */
|
||||
];
|
||||
|
||||
export type MethodType = 'async' | 'promise' | 'sync';
|
||||
|
||||
function genModule(config: ?ModuleConfig, moduleID: number): ?{name: string, module?: Object} {
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
|
||||
invariant(!moduleName.startsWith('RCT') && !moduleName.startsWith('RK'),
|
||||
'Module name prefixes should\'ve been stripped by the native side ' +
|
||||
'but wasn\'t for ' + moduleName);
|
||||
|
||||
if (!constants && !methods) {
|
||||
// Module contents will be filled in lazily later
|
||||
return { name: moduleName };
|
||||
}
|
||||
|
||||
const module = {};
|
||||
methods && methods.forEach((methodName, methodID) => {
|
||||
const isPromise = promiseMethods && arrayContains(promiseMethods, methodID);
|
||||
const isSync = syncMethods && arrayContains(syncMethods, methodID);
|
||||
invariant(!isPromise || !isSync, 'Cannot have a method that is both async and a sync hook');
|
||||
const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
|
||||
module[methodName] = genMethod(moduleID, methodID, methodType);
|
||||
});
|
||||
Object.assign(module, constants);
|
||||
|
||||
if (__DEV__) {
|
||||
BatchedBridge.createDebugLookup(moduleID, moduleName, methods);
|
||||
}
|
||||
|
||||
return { name: moduleName, module };
|
||||
}
|
||||
|
||||
function loadModule(name: string, moduleID: number): ?Object {
|
||||
invariant(global.nativeRequireModuleConfig,
|
||||
'Can\'t lazily create module without nativeRequireModuleConfig');
|
||||
const config = global.nativeRequireModuleConfig(name);
|
||||
const info = genModule(config, moduleID);
|
||||
return info && info.module;
|
||||
}
|
||||
|
||||
function genMethod(moduleID: number, methodID: number, type: MethodType) {
|
||||
let fn = null;
|
||||
if (type === 'promise') {
|
||||
fn = function(...args: Array<any>) {
|
||||
return new Promise((resolve, reject) => {
|
||||
BatchedBridge.enqueueNativeCall(moduleID, methodID, args,
|
||||
(data) => resolve(data),
|
||||
(errorData) => reject(createErrorFromErrorData(errorData)));
|
||||
});
|
||||
};
|
||||
} else if (type === 'sync') {
|
||||
fn = function(...args: Array<any>) {
|
||||
return global.nativeCallSyncHook(moduleID, methodID, args);
|
||||
};
|
||||
} else {
|
||||
fn = function(...args: Array<any>) {
|
||||
const lastArg = args.length > 0 ? args[args.length - 1] : null;
|
||||
const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
|
||||
const hasSuccessCallback = typeof lastArg === 'function';
|
||||
const hasErrorCallback = typeof secondLastArg === 'function';
|
||||
hasErrorCallback && invariant(
|
||||
hasSuccessCallback,
|
||||
'Cannot have a non-function arg after a function arg.'
|
||||
);
|
||||
const onSuccess = hasSuccessCallback ? lastArg : null;
|
||||
const onFail = hasErrorCallback ? secondLastArg : null;
|
||||
const callbackCount = hasSuccessCallback + hasErrorCallback;
|
||||
args = args.slice(0, args.length - callbackCount);
|
||||
BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);
|
||||
};
|
||||
}
|
||||
fn.type = type;
|
||||
return fn;
|
||||
}
|
||||
|
||||
function arrayContains<T>(array: Array<T>, value: T): boolean {
|
||||
return array.indexOf(value) !== -1;
|
||||
}
|
||||
|
||||
function createErrorFromErrorData(errorData: {message: string}): Error {
|
||||
const {
|
||||
message,
|
||||
...extraErrorInfo,
|
||||
} = errorData;
|
||||
const error = new Error(message);
|
||||
(error:any).framesToPop = 1;
|
||||
return Object.assign(error, extraErrorInfo);
|
||||
}
|
||||
|
||||
const bridgeConfig = global.__fbBatchedBridgeConfig;
|
||||
invariant(bridgeConfig, '__fbBatchedBridgeConfig is not set, cannot invoke native modules');
|
||||
|
||||
const NativeModules : {[moduleName: string]: Object} = {};
|
||||
(bridgeConfig.remoteModuleConfig || []).forEach((config: ModuleConfig, moduleID: number) => {
|
||||
// Initially this config will only contain the module name when running in JSC. The actual
|
||||
// configuration of the module will be lazily loaded.
|
||||
const info = genModule(config, moduleID);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.module) {
|
||||
NativeModules[info.name] = info.module;
|
||||
}
|
||||
// If there's no module config, define a lazy getter
|
||||
else {
|
||||
defineLazyObjectProperty(NativeModules, info.name, {
|
||||
get: () => loadModule(info.name, moduleID)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NativeModules;
|
||||
|
|
|
@ -1,17 +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 NativeModules
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const BatchedBridge = require('BatchedBridge');
|
||||
const RemoteModules = BatchedBridge.RemoteModules;
|
||||
|
||||
module.exports = RemoteModules;
|
|
@ -10,8 +10,8 @@
|
|||
*/
|
||||
'use strict';
|
||||
var remoteModulesConfig = [
|
||||
['RemoteModule1',null,['remoteMethod1','remoteMethod2'],[],[]],
|
||||
['RemoteModule2',null,['remoteMethod1','remoteMethod2'],[],[]],
|
||||
['RemoteModule1',null,['remoteMethod','promiseMethod'],[]],
|
||||
['RemoteModule2',null,['remoteMethod','promiseMethod'],[]],
|
||||
];
|
||||
|
||||
var MessageQueueTestConfig = {
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Copyright (c) 2013-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.unmock('BatchedBridge');
|
||||
jest.unmock('defineLazyObjectProperty');
|
||||
jest.unmock('MessageQueue');
|
||||
jest.unmock('NativeModules');
|
||||
|
||||
let BatchedBridge;
|
||||
let NativeModules;
|
||||
|
||||
const MODULE_IDS = 0;
|
||||
const METHOD_IDS = 1;
|
||||
const PARAMS = 2;
|
||||
const CALL_ID = 3;
|
||||
|
||||
const 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);
|
||||
};
|
||||
|
||||
// Important things to test:
|
||||
//
|
||||
// [x] Calling remote method actually queues it up on the BatchedBridge
|
||||
//
|
||||
// [x] Both error and success callbacks are invoked.
|
||||
//
|
||||
// [x] When simulating an error callback from remote method, both error and
|
||||
// success callbacks are cleaned up.
|
||||
//
|
||||
// [ ] Remote invocation throws if not supplying an error callback.
|
||||
describe('MessageQueue', function() {
|
||||
beforeEach(function() {
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
global.__fbBatchedBridgeConfig = require('MessageQueueTestConfig');
|
||||
BatchedBridge = require('BatchedBridge');
|
||||
NativeModules = require('NativeModules');
|
||||
});
|
||||
|
||||
it('should generate native modules', () => {
|
||||
NativeModules.RemoteModule1.remoteMethod('foo');
|
||||
const flushedQueue = BatchedBridge.flushedQueue();
|
||||
assertQueue(flushedQueue, 0, 0, 0, ['foo']);
|
||||
});
|
||||
|
||||
it('should make round trip and clear memory', function() {
|
||||
const onFail = jasmine.createSpy();
|
||||
const onSucc = jasmine.createSpy();
|
||||
|
||||
// Perform communication
|
||||
NativeModules.RemoteModule1.promiseMethod('paloAlto', 'menloPark', onFail, onSucc);
|
||||
NativeModules.RemoteModule2.promiseMethod('mac', 'windows', onFail, onSucc);
|
||||
|
||||
const resultingRemoteInvocations = BatchedBridge.flushedQueue();
|
||||
|
||||
// As always, the message queue has four fields
|
||||
expect(resultingRemoteInvocations.length).toBe(4);
|
||||
expect(resultingRemoteInvocations[MODULE_IDS].length).toBe(2);
|
||||
expect(resultingRemoteInvocations[METHOD_IDS].length).toBe(2);
|
||||
expect(resultingRemoteInvocations[PARAMS].length).toBe(2);
|
||||
expect(typeof resultingRemoteInvocations[CALL_ID]).toEqual('number');
|
||||
|
||||
expect(resultingRemoteInvocations[0][0]).toBe(0); // `RemoteModule1`
|
||||
expect(resultingRemoteInvocations[1][0]).toBe(1); // `promiseMethod`
|
||||
expect([ // the arguments
|
||||
resultingRemoteInvocations[2][0][0],
|
||||
resultingRemoteInvocations[2][0][1]
|
||||
]).toEqual(['paloAlto', 'menloPark']);
|
||||
// Callbacks ids are tacked onto the end of the remote arguments.
|
||||
const firstFailCBID = resultingRemoteInvocations[2][0][2];
|
||||
const firstSuccCBID = resultingRemoteInvocations[2][0][3];
|
||||
|
||||
expect(resultingRemoteInvocations[0][1]).toBe(1); // `RemoteModule2`
|
||||
expect(resultingRemoteInvocations[1][1]).toBe(1); // `promiseMethod`
|
||||
expect([ // the arguments
|
||||
resultingRemoteInvocations[2][1][0],
|
||||
resultingRemoteInvocations[2][1][1]
|
||||
]).toEqual(['mac', 'windows']);
|
||||
const secondFailCBID = resultingRemoteInvocations[2][1][2];
|
||||
const secondSuccCBID = resultingRemoteInvocations[2][1][3];
|
||||
|
||||
// Handle the first remote invocation by signaling failure.
|
||||
BatchedBridge.__invokeCallback(firstFailCBID, ['firstFailure']);
|
||||
// The failure callback was already invoked, the success is no longer valid
|
||||
expect(function() {
|
||||
BatchedBridge.__invokeCallback(firstSuccCBID, ['firstSucc']);
|
||||
}).toThrow();
|
||||
expect(onFail.calls.count()).toBe(1);
|
||||
expect(onSucc.calls.count()).toBe(0);
|
||||
|
||||
// Handle the second remote invocation by signaling success.
|
||||
BatchedBridge.__invokeCallback(secondSuccCBID, ['secondSucc']);
|
||||
// The success callback was already invoked, the fail cb is no longer valid
|
||||
expect(function() {
|
||||
BatchedBridge.__invokeCallback(secondFailCBID, ['secondFail']);
|
||||
}).toThrow();
|
||||
expect(onFail.calls.count()).toBe(1);
|
||||
expect(onSucc.calls.count()).toBe(1);
|
||||
});
|
||||
});
|
|
@ -19,21 +19,9 @@ const JSTimersExecution = require('JSTimersExecution');
|
|||
const Systrace = require('Systrace');
|
||||
|
||||
const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
|
||||
const defineLazyObjectProperty = require('defineLazyObjectProperty');
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
const stringifySafe = require('stringifySafe');
|
||||
|
||||
export type ConfigProvider = () => {
|
||||
remoteModuleConfig: Array<ModuleConfig>,
|
||||
};
|
||||
export type MethodType = 'async' | 'promise' | 'sync';
|
||||
export type ModuleConfig = [
|
||||
string, /* name */
|
||||
?Object, /* constants */
|
||||
Array<string>, /* functions */
|
||||
Array<number>, /* promise method IDs */
|
||||
Array<number>, /* sync method IDs */
|
||||
];
|
||||
export type SpyData = {
|
||||
type: number,
|
||||
module: ?string,
|
||||
|
@ -63,22 +51,20 @@ const guard = (fn) => {
|
|||
|
||||
class MessageQueue {
|
||||
_callableModules: {[key: string]: Object};
|
||||
_queue: [Array<number>, Array<number>, Array<any>];
|
||||
_queue: [Array<number>, Array<number>, Array<any>, number];
|
||||
_callbacks: [];
|
||||
_callbackID: number;
|
||||
_callID: number;
|
||||
_lastFlush: number;
|
||||
_eventLoopStartTime: number;
|
||||
|
||||
RemoteModules: Object;
|
||||
|
||||
_debugInfo: Object;
|
||||
_remoteModuleTable: Object;
|
||||
_remoteMethodTable: Object;
|
||||
|
||||
__spy: ?(data: SpyData) => void;
|
||||
|
||||
constructor(configProvider: ConfigProvider) {
|
||||
constructor() {
|
||||
this._callableModules = {};
|
||||
this._queue = [[], [], [], 0];
|
||||
this._callbacks = [];
|
||||
|
@ -97,11 +83,6 @@ class MessageQueue {
|
|||
(this:any).callFunctionReturnResultAndFlushedQueue = this.callFunctionReturnResultAndFlushedQueue.bind(this);
|
||||
(this:any).flushedQueue = this.flushedQueue.bind(this);
|
||||
(this:any).invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind(this);
|
||||
|
||||
defineLazyObjectProperty(this, 'RemoteModules', {get: () => {
|
||||
const {remoteModuleConfig} = configProvider();
|
||||
return this._genModules(remoteModuleConfig);
|
||||
}});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,7 +92,7 @@ class MessageQueue {
|
|||
static spy(spyOrToggle: boolean|(data: SpyData) => void){
|
||||
if (spyOrToggle === true){
|
||||
MessageQueue.prototype.__spy = info => {
|
||||
console.log(`${info.type == TO_JS ? 'N->JS' : 'JS->N'} : ` +
|
||||
console.log(`${info.type === TO_JS ? 'N->JS' : 'JS->N'} : ` +
|
||||
`${info.module ? (info.module + '.') : ''}${info.method}` +
|
||||
`(${JSON.stringify(info.args)})`);
|
||||
};
|
||||
|
@ -158,19 +139,6 @@ class MessageQueue {
|
|||
return queue[0].length ? queue : null;
|
||||
}
|
||||
|
||||
processModuleConfig(config: ModuleConfig, moduleID: number) {
|
||||
const info = this._genModule(config, moduleID);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.RemoteModules[info.name] = info.module;
|
||||
if (__DEV__) {
|
||||
this._createDebugLookup(config, moduleID);
|
||||
}
|
||||
return info.module;
|
||||
}
|
||||
|
||||
getEventLoopRunningTime() {
|
||||
return new Date().getTime() - this._eventLoopStartTime;
|
||||
}
|
||||
|
@ -179,16 +147,7 @@ class MessageQueue {
|
|||
this._callableModules[name] = module;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Private" methods
|
||||
*/
|
||||
__callImmediates() {
|
||||
Systrace.beginEvent('JSTimersExecution.callImmediates()');
|
||||
guard(() => JSTimersExecution.callImmediates());
|
||||
Systrace.endEvent();
|
||||
}
|
||||
|
||||
__nativeCall(moduleID: number, methodID: number, params: Array<any>, onFail: ?Function, onSucc: ?Function) {
|
||||
enqueueNativeCall(moduleID: number, methodID: number, params: Array<any>, onFail: ?Function, onSucc: ?Function) {
|
||||
if (onFail || onSucc) {
|
||||
if (__DEV__) {
|
||||
const callId = this._callbackID >> 1;
|
||||
|
@ -239,6 +198,23 @@ class MessageQueue {
|
|||
}
|
||||
}
|
||||
|
||||
createDebugLookup(moduleID: number, name: string, methods: Array<string>) {
|
||||
if (__DEV__) {
|
||||
this._remoteModuleTable[moduleID] = name;
|
||||
this._remoteMethodTable[moduleID] = methods;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Private" methods
|
||||
*/
|
||||
|
||||
__callImmediates() {
|
||||
Systrace.beginEvent('JSTimersExecution.callImmediates()');
|
||||
guard(() => JSTimersExecution.callImmediates());
|
||||
Systrace.endEvent();
|
||||
}
|
||||
|
||||
__callFunction(module: string, method: string, args: Array<any>) {
|
||||
this._lastFlush = new Date().getTime();
|
||||
this._eventLoopStartTime = this._lastFlush;
|
||||
|
@ -302,113 +278,6 @@ class MessageQueue {
|
|||
Systrace.endEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper methods
|
||||
*/
|
||||
|
||||
_genModules(remoteModules) {
|
||||
const modules = {};
|
||||
remoteModules.forEach((config, moduleID) => {
|
||||
// Initially this config will only contain the module name when running in JSC. The actual
|
||||
// configuration of the module will be lazily loaded (see NativeModules.js) and updated
|
||||
// through processModuleConfig.
|
||||
const info = this._genModule(config, moduleID);
|
||||
if (info) {
|
||||
modules[info.name] = info.module;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
this._createDebugLookup(config, moduleID);
|
||||
}
|
||||
});
|
||||
return modules;
|
||||
}
|
||||
|
||||
_genModule(config: ModuleConfig, moduleID: number): ?{name: string, module: Object} {
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
|
||||
|
||||
const module = {};
|
||||
methods && methods.forEach((methodName, methodID) => {
|
||||
const isPromise = promiseMethods && arrayContains(promiseMethods, methodID);
|
||||
const isSync = syncMethods && arrayContains(syncMethods, methodID);
|
||||
invariant(!isPromise || !isSync, 'Cannot have a method that is both async and a sync hook');
|
||||
const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
|
||||
module[methodName] = this._genMethod(moduleID, methodID, methodType);
|
||||
});
|
||||
Object.assign(module, constants);
|
||||
|
||||
if (!constants && !methods) {
|
||||
// Module contents will be filled in lazily later (see NativeModules)
|
||||
module.moduleID = moduleID;
|
||||
}
|
||||
|
||||
return { name: moduleName, module };
|
||||
}
|
||||
|
||||
_genMethod(moduleID: number, methodID: number, type: MethodType) {
|
||||
let fn = null;
|
||||
const self = this;
|
||||
if (type === 'promise') {
|
||||
fn = function(...args: Array<any>) {
|
||||
return new Promise((resolve, reject) => {
|
||||
self.__nativeCall(moduleID, methodID, args,
|
||||
(data) => resolve(data),
|
||||
(errorData) => reject(createErrorFromErrorData(errorData)));
|
||||
});
|
||||
};
|
||||
} else if (type === 'sync') {
|
||||
fn = function(...args: Array<any>) {
|
||||
return global.nativeCallSyncHook(moduleID, methodID, args);
|
||||
};
|
||||
} else {
|
||||
fn = function(...args: Array<any>) {
|
||||
const lastArg = args.length > 0 ? args[args.length - 1] : null;
|
||||
const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
|
||||
const hasSuccessCallback = typeof lastArg === 'function';
|
||||
const hasErrorCallback = typeof secondLastArg === 'function';
|
||||
hasErrorCallback && invariant(
|
||||
hasSuccessCallback,
|
||||
'Cannot have a non-function arg after a function arg.'
|
||||
);
|
||||
const onSuccess = hasSuccessCallback ? lastArg : null;
|
||||
const onFail = hasErrorCallback ? secondLastArg : null;
|
||||
const callbackCount = hasSuccessCallback + hasErrorCallback;
|
||||
args = args.slice(0, args.length - callbackCount);
|
||||
return self.__nativeCall(moduleID, methodID, args, onFail, onSuccess);
|
||||
};
|
||||
}
|
||||
fn.type = type;
|
||||
return fn;
|
||||
}
|
||||
|
||||
_createDebugLookup(config: ModuleConfig, moduleID: number) {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [moduleName, , methods] = config;
|
||||
this._remoteModuleTable[moduleID] = moduleName;
|
||||
this._remoteMethodTable[moduleID] = methods;
|
||||
}
|
||||
}
|
||||
|
||||
function arrayContains<T>(array: Array<T>, value: T): boolean {
|
||||
return array.indexOf(value) !== -1;
|
||||
}
|
||||
|
||||
function createErrorFromErrorData(errorData: {message: string}): Error {
|
||||
const {
|
||||
message,
|
||||
...extraErrorInfo,
|
||||
} = errorData;
|
||||
const error = new Error(message);
|
||||
(error:any).framesToPop = 1;
|
||||
return Object.assign(error, extraErrorInfo);
|
||||
}
|
||||
|
||||
module.exports = MessageQueue;
|
||||
|
|
|
@ -13,11 +13,13 @@
|
|||
|
||||
const NativeModules = require('NativeModules');
|
||||
const Platform = require('Platform');
|
||||
const { UIManager } = NativeModules;
|
||||
|
||||
const defineLazyObjectProperty = require('defineLazyObjectProperty');
|
||||
const findNodeHandle = require('react/lib/findNodeHandle');
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
const { UIManager } = NativeModules;
|
||||
|
||||
invariant(UIManager, 'UIManager is undefined. The native module config is probably incorrect.');
|
||||
|
||||
const _takeSnapshot = UIManager.takeSnapshot;
|
||||
|
@ -67,17 +69,10 @@ if (Platform.OS === 'ios') {
|
|||
Object.keys(UIManager).forEach(viewName => {
|
||||
const viewConfig = UIManager[viewName];
|
||||
if (viewConfig.Manager) {
|
||||
let constants;
|
||||
/* $FlowFixMe - nice try. Flow doesn't like getters */
|
||||
Object.defineProperty(viewConfig, 'Constants', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
defineLazyObjectProperty(viewConfig, 'Constants', {
|
||||
get: () => {
|
||||
if (constants) {
|
||||
return constants;
|
||||
}
|
||||
constants = {};
|
||||
const viewManager = NativeModules[viewConfig.Manager];
|
||||
const constants = {};
|
||||
viewManager && Object.keys(viewManager).forEach(key => {
|
||||
const value = viewManager[key];
|
||||
if (typeof value !== 'function') {
|
||||
|
@ -87,17 +82,10 @@ if (Platform.OS === 'ios') {
|
|||
return constants;
|
||||
},
|
||||
});
|
||||
let commands;
|
||||
/* $FlowFixMe - nice try. Flow doesn't like getters */
|
||||
Object.defineProperty(viewConfig, 'Commands', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
defineLazyObjectProperty(viewConfig, 'Commands', {
|
||||
get: () => {
|
||||
if (commands) {
|
||||
return commands;
|
||||
}
|
||||
commands = {};
|
||||
const viewManager = NativeModules[viewConfig.Manager];
|
||||
const commands = {};
|
||||
let index = 0;
|
||||
viewManager && Object.keys(viewManager).forEach(key => {
|
||||
const value = viewManager[key];
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* 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 MessageQueueTestModule1
|
||||
* @providesModule MessageQueueTestModule
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -15,12 +15,11 @@
|
|||
* correctly dispatches to commonJS modules. The `testHook` is overriden by test
|
||||
* cases.
|
||||
*/
|
||||
var MessageQueueTestModule1 = {
|
||||
var MessageQueueTestModule = {
|
||||
testHook1: function() {
|
||||
},
|
||||
testHook2: function() {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = MessageQueueTestModule1;
|
||||
|
||||
module.exports = MessageQueueTestModule;
|
|
@ -1,20 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2013-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 MessageQueueTestModule2
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var MessageQueueTestModule2 = {
|
||||
runLocalCode: function() {
|
||||
},
|
||||
runLocalCode2: function() {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = MessageQueueTestModule2;
|
|
@ -9,13 +9,12 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
const MessageQueueTestConfig = require('MessageQueueTestConfig');
|
||||
// const MessageQueueTestConfig = require('MessageQueueTestConfig');
|
||||
jest.unmock('MessageQueue');
|
||||
jest.unmock('defineLazyObjectProperty');
|
||||
|
||||
let MessageQueue;
|
||||
let MessageQueueTestModule1;
|
||||
let MessageQueueTestModule2;
|
||||
let MessageQueueTestModule;
|
||||
let queue;
|
||||
|
||||
const MODULE_IDS = 0;
|
||||
|
@ -30,169 +29,59 @@ const assertQueue = (flushedQueue, index, moduleID, methodID, params) => {
|
|||
|
||||
// Important things to test:
|
||||
//
|
||||
// [x] Calling remote method on queue actually queues it up.
|
||||
//
|
||||
// [x] Both error and success callbacks are invoked.
|
||||
//
|
||||
// [x] When simulating an error callback from remote method, both error and
|
||||
// success callbacks are cleaned up.
|
||||
//
|
||||
// [x] Local modules can be invoked through the queue.
|
||||
//
|
||||
// [ ] Local modules that throw exceptions are gracefully caught. In that case
|
||||
// local callbacks stored by IDs are cleaned up.
|
||||
//
|
||||
// [ ] Remote invocation throws if not supplying an error callback.
|
||||
describe('MessageQueue', function() {
|
||||
beforeEach(function() {
|
||||
jest.resetModuleRegistry();
|
||||
MessageQueue = require('MessageQueue');
|
||||
MessageQueueTestModule1 = require('MessageQueueTestModule1');
|
||||
MessageQueueTestModule2 = require('MessageQueueTestModule2');
|
||||
queue = new MessageQueue(
|
||||
() => MessageQueueTestConfig
|
||||
);
|
||||
|
||||
MessageQueueTestModule = require('MessageQueueTestModule');
|
||||
queue = new MessageQueue();
|
||||
queue.registerCallableModule(
|
||||
'MessageQueueTestModule1',
|
||||
MessageQueueTestModule1
|
||||
);
|
||||
queue.registerCallableModule(
|
||||
'MessageQueueTestModule2',
|
||||
MessageQueueTestModule2
|
||||
'MessageQueueTestModule',
|
||||
MessageQueueTestModule
|
||||
);
|
||||
queue.createDebugLookup(0, 'MessageQueueTestModule',
|
||||
['testHook1', 'testHook2']);
|
||||
});
|
||||
|
||||
it('should enqueue native calls', () => {
|
||||
queue.__nativeCall(0, 1, [2]);
|
||||
queue.enqueueNativeCall(0, 1, [2]);
|
||||
const flushedQueue = queue.flushedQueue();
|
||||
assertQueue(flushedQueue, 0, 0, 1, [2]);
|
||||
});
|
||||
|
||||
it('should call a local function with the function name', () => {
|
||||
MessageQueueTestModule1.testHook2 = jasmine.createSpy();
|
||||
expect(MessageQueueTestModule1.testHook2.calls.count()).toEqual(0);
|
||||
queue.__callFunction('MessageQueueTestModule1', 'testHook2', [2]);
|
||||
expect(MessageQueueTestModule1.testHook2.calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it('should generate native modules', () => {
|
||||
queue.RemoteModules.RemoteModule1.remoteMethod1('foo');
|
||||
const flushedQueue = queue.flushedQueue();
|
||||
assertQueue(flushedQueue, 0, 0, 0, ['foo']);
|
||||
MessageQueueTestModule.testHook2 = jasmine.createSpy();
|
||||
expect(MessageQueueTestModule.testHook2.calls.count()).toEqual(0);
|
||||
queue.__callFunction('MessageQueueTestModule', 'testHook2', [2]);
|
||||
expect(MessageQueueTestModule.testHook2.calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it('should store callbacks', () => {
|
||||
queue.RemoteModules.RemoteModule1.remoteMethod2('foo', () => {}, () => {});
|
||||
queue.enqueueNativeCall(0, 1, ['foo'], null, null);
|
||||
const flushedQueue = queue.flushedQueue();
|
||||
assertQueue(flushedQueue, 0, 0, 1, ['foo', 0, 1]);
|
||||
assertQueue(flushedQueue, 0, 0, 1, ['foo']);
|
||||
});
|
||||
|
||||
it('should call the stored callback', () => {
|
||||
var done = false;
|
||||
queue.RemoteModules.RemoteModule1.remoteMethod1(() => { done = true; });
|
||||
let done = false;
|
||||
queue.enqueueNativeCall(0, 1, [], () => {}, () => { done = true; });
|
||||
queue.__invokeCallback(1);
|
||||
expect(done).toEqual(true);
|
||||
});
|
||||
|
||||
it('should throw when calling the same callback twice', () => {
|
||||
queue.RemoteModules.RemoteModule1.remoteMethod1(() => {});
|
||||
queue.enqueueNativeCall(0, 1, [], () => {}, () => {});
|
||||
queue.__invokeCallback(1);
|
||||
expect(() => queue.__invokeCallback(1)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when calling both success and failure callback', () => {
|
||||
queue.RemoteModules.RemoteModule1.remoteMethod1(() => {}, () => {});
|
||||
queue.enqueueNativeCall(0, 1, [], () => {}, () => {});
|
||||
queue.__invokeCallback(1);
|
||||
expect(() => queue.__invokeCallback(0)).toThrow();
|
||||
});
|
||||
|
||||
it('should make round trip and clear memory', function() {
|
||||
// Perform communication
|
||||
|
||||
// First we're going to call into this (overriden) test hook pretending to
|
||||
// be a remote module making a "local" invocation into JS.
|
||||
const onFail = jasmine.createSpy();
|
||||
const onSucc = jasmine.createSpy();
|
||||
MessageQueueTestModule1.testHook1 = function() {
|
||||
// Then inside of this local module, we're going to fire off a remote
|
||||
// request.
|
||||
queue.__nativeCall(
|
||||
0,
|
||||
0,
|
||||
Array.prototype.slice.apply(arguments),
|
||||
onFail,
|
||||
onSucc,
|
||||
);
|
||||
};
|
||||
|
||||
// The second test hook does the same thing as the first, but fires off a
|
||||
// remote request to a different remote module/method.
|
||||
MessageQueueTestModule1.testHook2 = function() {
|
||||
queue.__nativeCall(
|
||||
1,
|
||||
1,
|
||||
Array.prototype.slice.apply(arguments),
|
||||
onFail,
|
||||
onSucc,
|
||||
);
|
||||
};
|
||||
|
||||
/* MessageQueueTestModule1.testHook1 */
|
||||
queue.__callFunction('MessageQueueTestModule1', 'testHook1', ['paloAlto', 'menloPark']);
|
||||
/* MessageQueueTestModule1.testHook2 */
|
||||
queue.__callFunction('MessageQueueTestModule1', 'testHook2', ['mac', 'windows']);
|
||||
|
||||
// And how do we know that it executed those local modules correctly? Well,
|
||||
// these particular test method echo their arguments back to remote methods!
|
||||
|
||||
var resultingRemoteInvocations = queue.flushedQueue();
|
||||
// As always, the message queue has five fields
|
||||
expect(resultingRemoteInvocations.length).toBe(4);
|
||||
expect(resultingRemoteInvocations[0].length).toBe(2);
|
||||
expect(resultingRemoteInvocations[1].length).toBe(2);
|
||||
expect(resultingRemoteInvocations[2].length).toBe(2);
|
||||
expect(typeof resultingRemoteInvocations[3]).toEqual('number');
|
||||
|
||||
expect(resultingRemoteInvocations[0][0]).toBe(0); // `RemoteModule1`
|
||||
expect(resultingRemoteInvocations[1][0]).toBe(0); // `remoteMethod1`
|
||||
expect([ // the arguments
|
||||
resultingRemoteInvocations[2][0][0],
|
||||
resultingRemoteInvocations[2][0][1]
|
||||
]).toEqual(['paloAlto', 'menloPark']);
|
||||
// Callbacks ids are tacked onto the end of the remote arguments.
|
||||
var firstFailCBID = resultingRemoteInvocations[2][0][2];
|
||||
var firstSuccCBID = resultingRemoteInvocations[2][0][3];
|
||||
|
||||
expect(resultingRemoteInvocations[0][1]).toBe(1); // `RemoteModule2`
|
||||
expect(resultingRemoteInvocations[1][1]).toBe(1); // `remoteMethod2`
|
||||
expect([ // the arguments
|
||||
resultingRemoteInvocations[2][1][0],
|
||||
resultingRemoteInvocations[2][1][1]
|
||||
]).toEqual(['mac', 'windows']);
|
||||
var secondFailCBID = resultingRemoteInvocations[2][1][2];
|
||||
var secondSuccCBID = resultingRemoteInvocations[2][1][3];
|
||||
|
||||
// Trigger init
|
||||
queue.RemoteModules;
|
||||
// Handle the first remote invocation by signaling failure.
|
||||
// -------------------------------------------------------
|
||||
queue.__invokeCallback(firstFailCBID, ['firstFailure']);
|
||||
// The failure callback was already invoked, the success is no longer valid
|
||||
expect(function() {
|
||||
queue.__invokeCallback(firstSuccCBID, ['firstSucc']);
|
||||
}).toThrow();
|
||||
expect(onFail.calls.count()).toBe(1);
|
||||
expect(onSucc.calls.count()).toBe(0);
|
||||
|
||||
// Handle the second remote invocation by signaling success.
|
||||
// -------------------------------------------------------
|
||||
queue.__invokeCallback(secondSuccCBID, ['secondSucc']);
|
||||
// The success callback was already invoked, the fail cb is no longer valid
|
||||
expect(function() {
|
||||
queue.__invokeCallback(secondFailCBID, ['secondFail']);
|
||||
}).toThrow();
|
||||
expect(onFail.calls.count()).toBe(1);
|
||||
expect(onSucc.calls.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue