react-native/Libraries/Utilities/MessageQueue.js
Dave Miller 9baff8f437 Make CameraRoll work with Promises
Summary:
public
This is the first module moving to the new model of working with Promises.

We now warn on uses of callback version.  At some point we will remove that.

Reviewed By: davidaurelio

Differential Revision: D2849811

fb-gh-sync-id: 8a31924cc2b438efc58f3ad22d5f27c273563472
2016-01-21 08:07:41 -08:00

385 lines
11 KiB
JavaScript

/**
* 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 MessageQueue
*/
/*eslint no-bitwise: 0*/
'use strict';
let Systrace = require('Systrace');
let ErrorUtils = require('ErrorUtils');
let JSTimersExecution = require('JSTimersExecution');
let Platform = require('Platform');
let invariant = require('invariant');
let keyMirror = require('keyMirror');
let stringifySafe = require('stringifySafe');
let MODULE_IDS = 0;
let METHOD_IDS = 1;
let PARAMS = 2;
let CALL_IDS = 3;
let MIN_TIME_BETWEEN_FLUSHES_MS = 5;
let TRACE_TAG_REACT_APPS = 1 << 17;
let SPY_MODE = false;
let MethodTypes = keyMirror({
remote: null,
remoteAsync: null,
});
var guard = (fn) => {
try {
fn();
} catch (error) {
ErrorUtils.reportFatalError(error);
}
};
class MessageQueue {
constructor(remoteModules, localModules) {
this.RemoteModules = {};
this._callableModules = {};
this._queue = [[], [], [], 0];
this._moduleTable = {};
this._methodTable = {};
this._callbacks = [];
this._callbackID = 0;
this._callID = 0;
this._lastFlush = 0;
this._eventLoopStartTime = new Date().getTime();
[
'invokeCallbackAndReturnFlushedQueue',
'callFunctionReturnFlushedQueue',
'flushedQueue',
].forEach((fn) => this[fn] = this[fn].bind(this));
let modulesConfig = this._genModulesConfig(remoteModules);
this._genModules(modulesConfig);
localModules && this._genLookupTables(
this._genModulesConfig(localModules),this._moduleTable, this._methodTable
);
this._debugInfo = {};
this._remoteModuleTable = {};
this._remoteMethodTable = {};
this._genLookupTables(
modulesConfig, this._remoteModuleTable, this._remoteMethodTable
);
}
/**
* Public APIs
*/
callFunctionReturnFlushedQueue(module, method, args) {
guard(() => {
this.__callFunction(module, method, args);
this.__callImmediates();
});
return this.flushedQueue();
}
invokeCallbackAndReturnFlushedQueue(cbID, args) {
guard(() => {
this.__invokeCallback(cbID, args);
this.__callImmediates();
});
return this.flushedQueue();
}
flushedQueue() {
this.__callImmediates();
let queue = this._queue;
this._queue = [[], [], [], this._callID];
return queue[0].length ? queue : null;
}
processModuleConfig(config, moduleID) {
const module = this._genModule(config, moduleID);
this._genLookup(config, moduleID, this._remoteModuleTable, this._remoteMethodTable);
return module;
}
getEventLoopRunningTime() {
return new Date().getTime() - this._eventLoopStartTime;
}
/**
* "Private" methods
*/
__callImmediates() {
Systrace.beginEvent('JSTimersExecution.callImmediates()');
guard(() => JSTimersExecution.callImmediates());
Systrace.endEvent();
}
__nativeCall(module, method, params, onFail, onSucc) {
if (onFail || onSucc) {
// 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;
}
global.nativeTraceBeginAsyncFlow &&
global.nativeTraceBeginAsyncFlow(TRACE_TAG_REACT_APPS, 'native', this._callID);
this._callID++;
this._queue[MODULE_IDS].push(module);
this._queue[METHOD_IDS].push(method);
this._queue[PARAMS].push(params);
var now = new Date().getTime();
if (global.nativeFlushQueueImmediate &&
now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) {
global.nativeFlushQueueImmediate(this._queue);
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
}
Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length);
if (__DEV__ && SPY_MODE && isFinite(module)) {
console.log('JS->N : ' + this._remoteModuleTable[module] + '.' +
this._remoteMethodTable[module][method] + '(' + JSON.stringify(params) + ')');
}
}
__callFunction(module, method, args) {
this._lastFlush = new Date().getTime();
this._eventLoopStartTime = this._lastFlush;
if (isFinite(module)) {
method = this._methodTable[module][method];
module = this._moduleTable[module];
}
Systrace.beginEvent(`${module}.${method}()`);
if (__DEV__ && SPY_MODE) {
console.log('N->JS : ' + module + '.' + method + '(' + JSON.stringify(args) + ')');
}
var moduleMethods = this._callableModules[module];
invariant(
!!moduleMethods,
'Module %s is not a registered callable module.',
module
);
moduleMethods[method].apply(moduleMethods, args);
Systrace.endEvent();
}
__invokeCallback(cbID, args) {
this._lastFlush = new Date().getTime();
this._eventLoopStartTime = this._lastFlush;
let callback = this._callbacks[cbID];
let debug = this._debugInfo[cbID >> 1];
let module = debug && this._remoteModuleTable[debug[0]];
let method = debug && this._remoteMethodTable[debug[0]][debug[1]];
invariant(
callback,
`Callback with id ${cbID}: ${module}.${method}() not found`
);
let profileName = debug ? '<callback for ' + module + '.' + method + '>' : cbID;
if (callback && SPY_MODE && __DEV__) {
console.log('N->JS : ' + profileName + '(' + JSON.stringify(args) + ')');
}
Systrace.beginEvent(
`MessageQueue.invokeCallback(${profileName}, ${stringifySafe(args)})`);
this._callbacks[cbID & ~1] = null;
this._callbacks[cbID | 1] = null;
callback.apply(null, args);
Systrace.endEvent();
}
/**
* Private helper methods
*/
/**
* Converts the old, object-based module structure to the new
* array-based structure. TODO (t8823865) Removed this
* function once Android has been updated.
*/
_genModulesConfig(modules /* array or object */) {
if (Array.isArray(modules)) {
return modules;
} else {
let moduleArray = [];
let moduleNames = Object.keys(modules);
for (var i = 0, l = moduleNames.length; i < l; i++) {
let moduleName = moduleNames[i];
let moduleConfig = modules[moduleName];
let module = [moduleName];
if (moduleConfig.constants) {
module.push(moduleConfig.constants);
}
let methodsConfig = moduleConfig.methods;
if (methodsConfig) {
let methods = [];
let asyncMethods = [];
let methodNames = Object.keys(methodsConfig);
for (var j = 0, ll = methodNames.length; j < ll; j++) {
let methodName = methodNames[j];
let methodConfig = methodsConfig[methodName];
methods[methodConfig.methodID] = methodName;
if (methodConfig.type === MethodTypes.remoteAsync) {
asyncMethods.push(methodConfig.methodID);
}
}
if (methods.length) {
module.push(methods);
if (asyncMethods.length) {
module.push(asyncMethods);
}
}
}
moduleArray[moduleConfig.moduleID] = module;
}
return moduleArray;
}
}
_genLookupTables(modulesConfig, moduleTable, methodTable) {
modulesConfig.forEach((config, moduleID) => {
this._genLookup(config, moduleID, moduleTable, methodTable);
});
}
_genLookup(config, moduleID, moduleTable, methodTable) {
if (!config) {
return;
}
let moduleName, methods;
if (moduleHasConstants(config)) {
[moduleName, , methods] = config;
} else {
[moduleName, methods] = config;
}
moduleTable[moduleID] = moduleName;
methodTable[moduleID] = Object.assign({}, methods);
}
_genModules(remoteModules) {
remoteModules.forEach((config, moduleID) => {
this._genModule(config, moduleID);
});
}
_genModule(config, moduleID) {
if (!config) {
return;
}
let moduleName, constants, methods, asyncMethods;
if (moduleHasConstants(config)) {
[moduleName, constants, methods, asyncMethods] = config;
} else {
[moduleName, methods, asyncMethods] = config;
}
let module = {};
methods && methods.forEach((methodName, methodID) => {
const methodType =
asyncMethods && arrayContains(asyncMethods, methodID) ?
MethodTypes.remoteAsync : MethodTypes.remote;
module[methodName] = this._genMethod(moduleID, methodID, methodType);
});
Object.assign(module, constants);
if (!constants && !methods && !asyncMethods) {
module.moduleID = moduleID;
}
this.RemoteModules[moduleName] = module;
return module;
}
_genMethod(module, method, type) {
let fn = null;
let self = this;
if (type === MethodTypes.remoteAsync) {
fn = function(...args) {
return new Promise((resolve, reject) => {
self.__nativeCall(
module,
method,
args,
(data) => {
// iOS always wraps the data in an Array regardless of what the
// shape of the data so we strip it out
// Android sends the data back properly
// TODO: Remove this once iOS has support for Promises natively (t9774697)
resolve(Platform.OS == 'ios' ? data[0] : data);
},
(errorData) => {
var error = createErrorFromErrorData(errorData);
reject(error);
});
});
};
} else {
fn = 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);
};
}
fn.type = type;
return fn;
}
registerCallableModule(name, methods) {
this._callableModules[name] = methods;
}
}
function moduleHasConstants(moduleArray: Array<Object|Array<>>): boolean {
return !Array.isArray(moduleArray[1]);
}
function arrayContains<T>(array: Array<T>, value: T): boolean {
return array.indexOf(value) !== -1;
}
function createErrorFromErrorData(errorData: {message: string}): Error {
var {
message,
...extraErrorInfo,
} = errorData;
var error = new Error(message);
error.framesToPop = 1;
return Object.assign(error, extraErrorInfo);
}
module.exports = MessageQueue;