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

@@ -0,0 +1,21 @@
/**
* 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.
*
* These don't actually exist anywhere in the code.
*/
'use strict';
var remoteModulesConfig = [
['RemoteModule1',null,['remoteMethod','promiseMethod'],[]],
['RemoteModule2',null,['remoteMethod','promiseMethod'],[]],
];
var MessageQueueTestConfig = {
remoteModuleConfig: remoteModulesConfig,
};
module.exports = 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);
});
});