Extract native module logic from BatchedBridge

Reviewed By: lexs

Differential Revision: D3901630

fbshipit-source-id: c119ffe54a4d1e716e6ae98895e5a3a48b16cf43
This commit is contained in:
Pieter De Baets 2016-09-23 11:12:54 -07:00 committed by Facebook Github Bot 8
parent 31b158c9fe
commit 76c54847bb
10 changed files with 288 additions and 376 deletions

View File

@ -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'));

View File

@ -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;

View File

@ -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;

View File

@ -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 = {

View File

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

View File

@ -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;

View File

@ -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];

View File

@ -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;

View File

@ -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;

View File

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