blob: 0be90a7e356e333e9287ef815933b939c7079060 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
*
* In Chrome Apps, some platform APIs can only be called from the background
* page (e.g. reloading a chrome.app.AppWindow). Likewise, some chrome API's
* must be initiated by user interaction, which can only be called from the
* foreground.
*
* This class provides helper functions to invoke methods on different pages
* using chrome.runtime.sendMessage. Messages are passed in the following
* format:
* {methodName:{string}, params:{Array}}
*
* chrome.runtime.sendMessage allows multiple handlers to be registered on a
* document, but only one handler can send a response.
* This class uniquely identifies a method with the |methodName| and enforces
* that only one handler can be registered per |methodName| in the document.
*
* For example, to call method foo() in the background page from the foreground
* chrome.app.AppWindow, you can do the following.
* In the background page:
* base.Ipc.getInstance().register('my.service.name', foo);
*
* In the AppWindow document:
* base.Ipc.invoke('my.service.name', arg1, arg2, ...).then(
* function(result) {
* console.log('The result is ' + result);
* });
*
* This will invoke foo() with the arg1, arg2, ....
* The return value of foo() will be passed back to the caller in the
* form of a promise.
*/
/** @suppress {duplicate} */
var base = base || {};
(function() {
'use strict';
/**
* @constructor
* @private
*/
base.Ipc = function() {
console.assert(instance_ === null, 'Duplicate base.Ipc constructor.');
/** @private {!Object<{allowExternal: boolean, method: Function>} */
this.handlers_ = {};
this.onMessageHandler_ =
/** @type {function(*, MessageSender, function (*))} */ (
this.onMessage_.bind(this));
chrome.runtime.onMessage.addListener(this.onMessageHandler_);
chrome.runtime.onMessageExternal.addListener(this.onMessageHandler_);
};
/** @private */
base.Ipc.prototype.dispose_ = function() {
chrome.runtime.onMessage.removeListener(this.onMessageHandler_);
chrome.runtime.onMessageExternal.removeListener(this.onMessageHandler_);
};
/**
* The error strings are only used for debugging purposes and are not localized.
*
* @enum {string}
*/
base.Ipc.Error = {
UNSUPPORTED_REQUEST_TYPE: 'Unsupported method name.',
UNAUTHORIZED_REQUEST_ORIGIN: 'Unauthorized origin.'
};
/**
* @constructor
* @param {string} methodName
* @param {?Array} params
* @struct
* @private
*/
base.Ipc.Request_ = function(methodName, params) {
this.methodName = methodName;
this.params = params;
};
/**
* @param {string} methodName
* @param {Function} handler The handler can be invoked by calling
* base.Ipc.invoke(|methodName|, arg1, arg2, ...)
* @param {boolean=} opt_allowExternal True if the message is permitted from
* origins listed in externally_connectable; false or undefined if the
* message is only permitted from the same app.
* @return {boolean} Whether the handler is successfully registered.
*/
base.Ipc.prototype.register = function(methodName, handler,
opt_allowExternal) {
if (methodName in this.handlers_) {
console.error('service ' + methodName + ' is already registered.');
return false;
}
this.handlers_[methodName] = {
allowExternal: Boolean(opt_allowExternal),
method: handler
};
return true;
};
/**
* @param {string} methodName
*/
base.Ipc.prototype.unregister = function(methodName) {
delete this.handlers_[methodName];
};
/**
* @param {base.Ipc.Request_} message
* @param {!MessageSender} sender
* @param {function(*): void} sendResponse
* @return {boolean} True if an asynchronous response is pending; false if the
* response (if any) has already been sent.
*/
base.Ipc.prototype.onMessage_ = function(message, sender, sendResponse) {
var methodName = message.methodName;
if (typeof methodName !== 'string') {
return;
}
var handler = this.handlers_[methodName];
if (!handler) {
sendResponse({error: base.Ipc.Error.UNSUPPORTED_REQUEST_TYPE});
return;
}
if (!handler.allowExternal && sender.id !== chrome.runtime.id) {
sendResponse({error: base.Ipc.Error.UNAUTHORIZED_REQUEST_ORIGIN});
return;
}
try {
var result = handler.method.apply(null, message.params);
if (result instanceof Promise) {
result.
then(function(response) { sendResponse(response); }).
catch(function(e) { sendResponse({error: e.message}) });
return true;
} else {
sendResponse(result);
}
} catch (/** @type {Error} */ e) {
sendResponse({error: e.message});
}
return false;
};
/**
* Invokes a method on another page within this extension.
*
* @param {string} methodName
* @param {...} var_args
* @return {Promise} A Promise that would resolve to the return value of the
* handler or reject if the handler throws an exception.
* @suppress {reportUnknownTypes}
*/
base.Ipc.invoke = function(methodName, var_args) {
var params = Array.prototype.slice.call(arguments, 1);
var sendMessage = base.Promise.as(
chrome.runtime.sendMessage,
[null, new base.Ipc.Request_(methodName, params)]);
return sendMessage.then(
/** @param {?{error: Error}} response */
function(response) {
if (response && response.error) {
return Promise.reject(response.error);
} else {
return Promise.resolve(response);
}
});
};
/** @type {base.Ipc} */
var instance_ = null;
/** @return {base.Ipc} */
base.Ipc.getInstance = function() {
if (!instance_) {
instance_ = new base.Ipc();
}
return instance_;
};
base.Ipc.deleteInstance = function() {
if (instance_) {
instance_.dispose_();
instance_ = null;
}
};
})();