blob: 00ad76ac4ca7e34b7a05a5d6cbbd912f0a86fc29 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var EE = require('eventemitter2').EventEmitter2;
var inherits = require('inherits');
var types = require('./event-proxy-message-types');
var extnUtils = require('./extension-utils');
var errors = require('../verror/index');
var defaultTimeout = 5000; // ms
// ExtensionEventProxy sends messages to the extension, and listens for messages
// coming from the extension.
function ExtensionEventProxy(timeout){
if (!(this instanceof ExtensionEventProxy)) {
return new ExtensionEventProxy(timeout);
}
if (typeof timeout === 'undefined') {
timeout = defaultTimeout;
}
EE.call(this);
var proxy = this;
this.onEvent = function(ev) {
proxy.emit(ev.detail.type, ev.detail.body);
};
window.addEventListener(types.TO_PAGE, this.onEvent);
this.waitingForExtension = true;
// Queue of messages to send once we know the extension event proxy is
// listening.
this.queuedMessages = [];
// Check to see if the extension is installed.
extnUtils.isExtensionInstalled(function(err, isInstalled) {
if (err) {
proxy.emit('error', err);
}
// If not installed, emit ExtensionNotInstalledError.
if (!isInstalled) {
proxy.emit('error', new errors.ExtensionNotInstalledError());
proxy._extensionNotInstalled = true;
return;
}
// Otherwise, wait until the extension has loaded and is responding to
// messages.
proxy.waitForExtension(timeout);
});
// Echo any errors or crashes we receive to the console.
this.on('error', function(err) {
console.error('Error message received from content script:', err);
});
this.on('crash', function(err) {
console.error('Crash message received from content script.');
if (err) {
console.error(err);
}
});
}
inherits(ExtensionEventProxy, EE);
ExtensionEventProxy.prototype.destroy = function() {
this.removeAllListeners();
window.removeEventListener(types.TO_PAGE, this.onEvent);
};
ExtensionEventProxy.prototype.send = function(type, body) {
// If we are still waiting for the extension, queue messages to be sent later.
if (this.waitingForExtension) {
this.queuedMessages.push({
type: type,
body: body
});
return;
}
window.dispatchEvent(
new window.CustomEvent(types.TO_EXTENSION, {
detail: {
type: type,
body: body
}
})
);
};
// Repeatedly ping the extension, and wait a specified time for it to respond.
// If we don't hear back, emit an error.
ExtensionEventProxy.prototype.waitForExtension = function(timeout) {
this.waitInterval = setInterval(function() {
window.dispatchEvent(new window.CustomEvent(types.EXTENSION_IS_READY));
}, 200);
var proxy = this;
this.waitTimeout = setTimeout(function() {
if (!proxy.waitingForExtension) {
return;
}
proxy.waitingForExtension = false;
clearInterval(proxy.waitInterval);
var error = new Error('Timeout waiting for extension.');
proxy.emit('error', error);
}, timeout);
// Once the extension is listening, clear the timeout and interval, and send
// queued messages.
window.addEventListener(types.EXTENSION_READY, function() {
if (!proxy.waitingForExtension) {
return;
}
proxy.waitingForExtension = false;
clearInterval(proxy.waitInterval);
clearTimeout(proxy.waitTimeout);
proxy.queuedMessages.forEach(function(msg) {
proxy.send(msg.type, msg.body);
});
proxy.queuedMessages = [];
proxy.emit('connected');
});
};
// Wrapper around 'send' method that will call callback with error and data when
// extension responds.
ExtensionEventProxy.prototype.sendRpc = function(type, data, cb) {
if (this._extensionNotInstalled) {
cb(new errors.ExtensionNotInstalledError());
return;
}
function onSuccess(data) {
removeListeners();
cb(null, data);
}
// Handle rpc-specific errors.
function onRpcError(data) {
removeListeners();
cb(objectToError(data.error));
}
// Handle errors and crashes, which can be triggered if the extension is not
// running or if it crashes during initialization.
function onError(err) {
removeListeners();
cb(objectToError(err));
}
var proxy = this;
function removeListeners() {
proxy.removeListener(type + ':success', onSuccess);
proxy.removeListener(type + ':error', onRpcError);
proxy.removeListener('crash', onError);
proxy.removeListener('error', onError);
}
this.on(type + ':success', onSuccess);
this.on(type + ':error', onRpcError);
this.on('crash', onError);
this.on('error', onError);
// Send request.
this.send(type, data);
};
// An error that gets sent via postMessage will be received as a plain Object.
// This function turns it back into an Error object.
function objectToError(obj) {
if (obj instanceof Error) {
return obj;
}
var err = new Error(obj.message);
err.name = obj.name;
err.stack = obj.stack;
return err;
}
module.exports = new ExtensionEventProxy();
module.exports.ctor = ExtensionEventProxy;