src/moduleinternal.js
/*jslint indent:2, node:true,sloppy:true */
var PromiseCompat = require('es6-promise').Promise;
var ApiInterface = require('./proxy/apiInterface');
var Provider = require('./provider');
var ProxyBinder = require('./proxybinder');
var util = require('./util');
/**
* The internal logic for module setup, which makes sure the public
* facing exports have appropriate properties, and load user scripts.
* @class ModuleInternal
* @extends Port
* @param {Port} manager The manager in this module to use for routing setup.
* @constructor
*/
var ModuleInternal = function (manager) {
this.config = {};
this.manager = manager;
this.debug = manager.debug;
this.binder = new ProxyBinder(this.manager);
this.api = this.manager.api;
this.manifests = {};
this.providers = {};
this.id = 'ModuleInternal';
this.pendingPorts = 0;
this.requests = {};
this.unboundPorts = {};
util.handleEvents(this);
};
/**
* Message handler for this port.
* This port only handles two messages:
* The first is its setup from the manager, which it uses for configuration.
* The second is from the module controller (fdom.port.Module), which provides
* the manifest info for the module.
* @method onMessage
* @param {String} flow The detination of the message.
* @param {Object} message The message.
*/
ModuleInternal.prototype.onMessage = function (flow, message) {
if (flow === 'control') {
if (!this.controlChannel && message.channel) {
this.controlChannel = message.channel;
util.mixin(this.config, message.config);
}
} else if (flow === 'default' && !this.appId) {
// Recover the ID of this module:
this.port = this.manager.hub.getDestination(message.channel);
this.externalChannel = message.channel;
this.appId = message.appId;
this.lineage = message.lineage;
var objects = this.mapProxies(message.manifest);
this.generateEnv(message.manifest, objects).then(function () {
return this.loadLinks(objects);
}.bind(this)).then(this.loadScripts.bind(this, message.id,
message.manifest.app.script)).then(null, function (err) {
this.debug.error('Could not set up module ' + this.appId + ': ', err);
}.bind(this));
} else if (flow === 'default' && message.type === 'resolve.response' &&
this.requests[message.id]) {
this.requests[message.id](message.data);
delete this.requests[message.id];
} else if (flow === 'default' && message.type === 'require.failure' &&
this.unboundPorts[message.id]) {
this.unboundPorts[message.id].callback(undefined, message.error);
delete this.unboundPorts[message.id];
} else if (flow === 'default' && message.type === 'manifest') {
this.emit('manifest', message);
this.updateManifest(message.name, message.manifest);
} else if (flow === 'default' && message.type === 'Connection') {
// Multiple connections can be made to the default provider.
if (message.api && this.providers[message.api]) {
this.manager.createLink(this.providers[message.api], message.channel,
this.port, message.channel);
} else if (this.defaultPort &&
(message.api === this.defaultPort.api || !message.api)) {
this.manager.createLink(this.defaultPort, message.channel,
this.port, message.channel);
} else {
this.once('start', this.onMessage.bind(this, flow, message));
}
}
};
/**
* Get a textual description of this Port.
* @method toString
* @return {String} a description of this Port.
*/
ModuleInternal.prototype.toString = function () {
return "[Environment Helper]";
};
/**
* Generate an externaly visisble namespace
* @method generateEnv
* @param {Object} manifest The manifest of the module.
* @param {Object[]} items Other interfaces to load.
* @returns {Promise} A promise when the external namespace is visible.
* @private
*/
ModuleInternal.prototype.generateEnv = function (manifest, items) {
return this.binder.bindDefault(this.port, this.api, manifest, true).then(
function (binding) {
var i = 0;
binding.port.api = binding.external.api;
this.defaultPort = binding.port;
if (binding.external.api) {
for (i = 0; i < items.length; i += 1) {
if (items[i].name === binding.external.api && items[i].def.provides) {
items.splice(i, 1);
break;
}
}
}
this.config.global.freedom = binding.external;
}.bind(this)
);
};
/**
* Register an unused channel ID for callback, and once information
* about the channel is known, call the handler with that information.
* @method registerId
* @param {String} api The preferred API to use for the new channel.
* @param {Function} callback Function to call once channel ready
* @returns {String} The allocated channel name.
*/
ModuleInternal.prototype.registerId = function (api, callback) {
var id = util.getId();
this.unboundPorts[id] = {
name: api,
callback: callback
};
return id;
};
/**
* Attach a proxy to the externally visible namespace.
* @method attach
* @param {String} name The name of the proxy.
* @param {Boolean} provides If this proxy is a provider.
* @param {ProxyInterface} proxy The proxy to attach.
* @param {String} api The API the proxy implements.
* @private.
*/
ModuleInternal.prototype.attach = function (name, provides, proxy) {
var exp = this.config.global.freedom;
if (provides) {
this.providers[name] = proxy.port;
}
if (!exp[name]) {
exp[name] = proxy.external;
if (this.manifests[name]) {
exp[name].manifest = this.manifests[name];
}
}
this.pendingPorts -= 1;
if (this.pendingPorts === 0) {
this.emit('start');
}
return exp[name];
};
/**
* Request a set of proxy interfaces, and bind them to the external
* namespace.
* @method loadLinks
* @param {Object[]} items Descriptors of the proxy ports to load.
* @private
* @returns {Promise} Promise for when all links are loaded.
*/
//TODO(willscott): promise should be chained, rather than going through events.
ModuleInternal.prototype.loadLinks = function (items) {
var i, proxy, provider, core,
manifestPredicate = function (name, flow, msg) {
return flow === 'manifest' && msg.name === name;
},
onManifest = function (item, msg) {
var definition = {
name: item.api
};
if (!msg.manifest.api || !msg.manifest.api[item.api]) {
definition.definition = null;
} else {
definition.definition = msg.manifest.api[item.api];
}
this.binder.getExternal(this.port, item.name, definition).then(
this.attach.bind(this, item.name, false)
);
}.bind(this),
promise = new PromiseCompat(function (resolve, reject) {
this.once('start', resolve);
}.bind(this));
for (i = 0; i < items.length; i += 1) {
if (items[i].api && !items[i].def) {
if (this.manifests[items[i].name]) {
onManifest(items[i], {
manifest: this.manifests[items[i].name]
});
} else {
this.once(manifestPredicate.bind({}, items[i].name),
onManifest.bind(this, items[i]));
}
} else {
this.binder.getExternal(this.port, items[i].name, items[i].def).then(
this.attach.bind(this, items[i].name, items[i].def &&
items[i].def.provides)
);
}
this.pendingPorts += 1;
}
// Allow resolution of files by parent.
this.manager.resource.addResolver(function (manifest, url, resolve) {
var id = util.getId();
this.requests[id] = resolve;
this.emit(this.externalChannel, {
type: 'resolve',
id: id,
data: url
});
return true;
}.bind(this));
// Attach Core.
this.pendingPorts += 1;
core = this.api.get('core').definition;
provider = new Provider(core, this.debug);
this.manager.getCore(function (CoreProv) {
new CoreProv(this.manager).setId(this.lineage, this);
provider.getInterface().provideAsynchronous(CoreProv);
}.bind(this));
this.emit(this.controlChannel, {
type: 'Link to core',
request: 'link',
name: 'core',
to: provider
});
this.binder.getExternal(provider, 'default', {
name: 'core',
definition: core
}).then(function (core) {
core.external.getLoggerSync = this.debug.getLoggingShim(
core.external().getLogger);
this.attach('core', false, core);
}.bind(this));
if (this.pendingPorts === 0) {
this.emit('start');
}
return promise;
};
/**
* Update the exported manifest of a dependency.
* Sets it internally if not yet exported, or attaches the property if it
* is loaded after the module has started (we don't delay start to retreive
* the manifest of the dependency.)
* @method updateManifest
* @param {String} name The Dependency
* @param {Object} manifest The manifest of the dependency
*/
ModuleInternal.prototype.updateManifest = function (name, manifest) {
var exp = this.config.global.freedom;
if (exp && exp[name]) {
exp[name].manifest = manifest;
// Handle require() dependency resolution.
} else if (this.unboundPorts[name]) {
this.binder.getExternal(this.port, name,
this.binder.getAPI(manifest, this.api, this.unboundPorts[name].api))
.then(
this.attach.bind(this, name, false)
).then(function(proxy) {
this.unboundPorts[name].callback(proxy);
delete this.unboundPorts[name];
}.bind(this));
} else {
this.manifests[name] = manifest;
}
};
/**
* Determine which proxy ports should be exposed by this module.
* @method mapProxies
* @param {Object} manifest the module JSON manifest.
* @return {Object[]} proxy descriptors defined in the manifest.
*/
ModuleInternal.prototype.mapProxies = function (manifest) {
var proxies = [], seen = ['core'], i, obj;
if (manifest.permissions) {
for (i = 0; i < manifest.permissions.length; i += 1) {
obj = {
name: manifest.permissions[i],
def: undefined
};
obj.def = this.api.get(obj.name);
if (seen.indexOf(obj.name) < 0 && obj.def) {
proxies.push(obj);
seen.push(obj.name);
}
}
}
if (manifest.dependencies) {
util.eachProp(manifest.dependencies, function (desc, name) {
obj = {
name: name,
api: desc.api
};
if (seen.indexOf(name) < 0) {
if (desc.api) {
obj.def = this.api.get(desc.api);
}
proxies.push(obj);
seen.push(name);
}
}.bind(this));
}
if (manifest.provides) {
for (i = 0; i < manifest.provides.length; i += 1) {
obj = {
name: manifest.provides[i],
def: undefined
};
obj.def = this.api.get(obj.name);
if (obj.def) {
obj.def.provides = true;
} else if (manifest.api && manifest.api[obj.name]) {
obj.def = {
name: obj.name,
definition: manifest.api[obj.name],
provides: true
};
} else {
this.debug.warn('Module will not provide "' + obj.name +
'", since no declaration can be found.');
/*jslint continue:true*/
continue;
}
/*jslint continue:false*/
if (seen.indexOf(obj.name) < 0) {
proxies.push(obj);
seen.push(obj.name);
}
}
}
return proxies;
};
/**
* Load external scripts into this namespace.
* @method loadScripts
* @param {String} from The URL of this modules's manifest.
* @param {String[]} scripts The URLs of the scripts to load.
*/
ModuleInternal.prototype.loadScripts = function (from, scripts) {
var importer = function (script, resolve, reject) {
try {
this.config.global.importScripts(script);
resolve(true);
} catch (e) {
reject(e);
}
}.bind(this),
scripts_count,
load;
if (typeof scripts === 'string') {
scripts_count = 1;
} else {
scripts_count = scripts.length;
}
load = function (next) {
if (next === scripts_count) {
this.emit(this.externalChannel, {
type: "ready"
});
return;
}
var script;
if (typeof scripts === 'string') {
script = scripts;
} else {
script = scripts[next];
}
this.manager.resource.get(from, script).then(function (url) {
this.tryLoad(importer, url).then(function () {
load(next + 1);
}.bind(this));
}.bind(this));
}.bind(this);
if (!this.config.global.importScripts) {
importer = function (url, resolve, reject) {
var script = this.config.global.document.createElement('script');
script.src = url;
script.addEventListener('load', resolve, true);
script.addEventListener('error', reject, true);
this.config.global.document.body.appendChild(script);
}.bind(this);
}
load(0);
};
/**
* Attempt to load resolved scripts into the namespace.
* @method tryLoad
* @private
* @param {Function} importer The actual import function
* @param {String[]} urls The resoved URLs to load.
* @returns {Promise} completion of load
*/
ModuleInternal.prototype.tryLoad = function (importer, url) {
return new PromiseCompat(importer.bind({}, url)).then(function (val) {
return val;
}, function (e) {
this.debug.warn(e.stack);
this.debug.error("Error loading " + url, e);
this.debug.error("If the stack trace is not useful, see https://" +
"github.com/freedomjs/freedom/wiki/Debugging");
// This event is caught in Module, which will then respond to any messages
// for the provider with short-circuit errors.
this.emit(this.externalChannel, {
type: 'error'
});
throw e;
}.bind(this));
};
module.exports = ModuleInternal;