src/policy.js
/*globals XMLHttpRequest */
/*jslint indent:2,white:true,node:true,sloppy:true */
var PromiseCompat = require('es6-promise').Promise;
var Module = require('./module');
var util = require('./util');
/**
* The Policy registry for freedom.js. Used to look up modules and provide
* migration and coallesing of execution.
* @Class Policy
* @param {Manager} manager The manager of the active runtime.
* @param {Resource} resource The resource loader of the active runtime.
* @param {Object} config The local config.
* @constructor
*/
var Policy = function(manager, resource, config) {
this.api = manager.api;
this.debug = manager.debug;
this.location = config.location;
this.resource = resource;
this.config = config;
this.runtimes = [];
this.policies = [];
this.pending = {};
util.handleEvents(this);
this.add(manager, config.policy);
this.runtimes[0].local = true;
};
/**
* The policy a runtime is expected to have unless it specifies
* otherwise.
* TODO: consider making static
* @property defaultPolicy
*/
Policy.prototype.defaultPolicy = {
background: false, // Can this runtime run 'background' modules?
interactive: true // Is there a view associated with this runtime?
// TODO: remaining runtime policy.
};
/**
* The constraints a code modules is expected to have unless it specifies
* otherwise.
* TODO: consider making static
* @property defaultConstraints
*/
Policy.prototype.defaultConstraints = {
isolation: "always", // values: always, app, never
placement: "local" // values: local, stable, redundant
// TODO: remaining constraints, express platform-specific dependencies.
};
/**
* Resolve a module from its canonical URL.
* Reponds with the promise of a port representing the module,
* @method get
* @param {String[]} lineage The lineage of the requesting module.
* @param {String} id The canonical ID of the module to get.
* @returns {Promise} A promise for the local port towards the module.
*/
Policy.prototype.get = function(lineage, id) {
// Make sure that a module isn't getting located twice at the same time.
// This is resolved by delaying if it until we see it in a 'moduleAdd' event.
if (this.pending[id]) {
return new PromiseCompat(function (resolve, reject) {
this.once('placed', function(l, i) {
this.get(l, i).then(resolve, reject);
}.bind(this, lineage, id));
}.bind(this));
} else {
this.pending[id] = true;
}
return this.loadManifest(id).then(function(manifest) {
var constraints = this.overlay(this.defaultConstraints, manifest.constraints),
runtime = this.findDestination(lineage, id, constraints),
portId;
if (runtime.local) {
portId = this.isRunning(runtime, id, lineage,
constraints.isolation !== 'never');
if(constraints.isolation !== 'always' && portId) {
this.debug.info('Reused port ' + portId);
delete this.pending[id];
this.emit('placed');
return runtime.manager.getPort(portId);
} else {
return new Module(id, manifest, lineage, this);
}
} else {
// TODO: Create a port to go to the remote runtime.
this.debug.error('Unexpected location selected for module placement');
return false;
}
}.bind(this), function(err) {
this.debug.error('Policy Error Resolving ' + id, err);
throw(err);
}.bind(this));
};
/**
* Find the runtime destination for a module given its constraints and the
* module creating it.
* @method findDestination
* @param {String[]} lineage The identity of the module creating this module.
* @param {String] id The canonical url of the module
* @param {Object} constraints Constraints for the module.
* @returns {Object} The element of this.runtimes where the module should run.
*/
Policy.prototype.findDestination = function(lineage, id, constraints) {
var i;
// Step 1: if an instance already exists, the m
if (constraints.isolation !== 'always') {
for (i = 0; i < this.policies.length; i += 1) {
if (this.isRunning(this.runtimes[i], id, lineage,
constraints.isolation !== 'never')) {
return this.runtimes[i];
}
}
}
// Step 2: if the module wants stability, it may need to be remote.
if (constraints.placement === 'local') {
return this.runtimes[0];
} else if (constraints.placement === 'stable') {
for (i = 0; i < this.policies.length; i += 1) {
if (this.policies[i].background) {
return this.runtimes[i];
}
}
}
// Step 3: if the module needs longevity / interactivity, it may want to be remote.
return this.runtimes[0];
};
/**
* Determine if a known runtime is running an appropriate instance of a module.
* @method isRunning
* @param {Object} runtime The runtime to check.
* @param {String} id The module to look for.
* @param {String[]} from The identifier of the requesting module.
* @param {Boolean} fullMatch If the module needs to be in the same app.
* @returns {String|Boolean} The Module id if it is running, or false if not.
*/
Policy.prototype.isRunning = function(runtime, id, from, fullMatch) {
var i = 0, j = 0, okay;
for (i = 0; i < runtime.modules.length; i += 1) {
if (fullMatch && runtime.modules[i].length === from.length + 1) {
okay = true;
for (j = 0; j < from.length; j += 1) {
if (runtime.modules[i][j + 1].indexOf(from[j]) !== 0) {
okay = false;
break;
}
}
if (runtime.modules[i][0].indexOf(id) !== 0) {
okay = false;
}
if (okay) {
return runtime.modules[i][0];
}
} else if (!fullMatch && runtime.modules[i][0].indexOf(id) === 0) {
return runtime.modules[i][0];
}
}
return false;
};
/**
* Get a promise of the manifest for a module ID.
* @method loadManifest
* @param {String} manifest The canonical ID of the manifest
* @returns {Promise} Promise for the json contents of the manifest.
*/
Policy.prototype.loadManifest = function(manifest) {
return this.resource.getContents(manifest).then(function(data) {
var resp = {};
try {
return JSON.parse(data);
} catch(err) {
this.debug.error("Failed to load " + manifest + ": " + err);
throw new Error("No Manifest Available");
}
}.bind(this));
};
/**
* Add a runtime to keep track of in this policy.
* @method add
* @param {fdom.port} port The port to use for module lifetime info
* @param {Object} policy The policy of the runtime.
*/
Policy.prototype.add = function(port, policy) {
var runtime = {
manager: port,
modules: []
};
this.runtimes.push(runtime);
this.policies.push(this.overlay(this.defaultPolicy, policy));
port.on('moduleAdd', function(runtime, info) {
var lineage = [];
lineage = lineage.concat(info.lineage);
lineage[0] = info.id;
runtime.modules.push(lineage);
if (this.pending[info.lineage[0]]) {
delete this.pending[info.lineage[0]];
this.emit('placed');
}
}.bind(this, runtime));
port.on('moduleRemove', function(runtime, info) {
var lineage = [], i, modFingerprint;
lineage = lineage.concat(info.lineage);
lineage[0] = info.id;
modFingerprint = lineage.toString();
for (i = 0; i < runtime.modules.length; i += 1) {
if (runtime.modules[i].toString() === modFingerprint) {
runtime.modules.splice(i, 1);
return;
}
}
this.debug.warn('Unknown module to remove: ', info.id);
}.bind(this, runtime));
};
/**
* Overlay a specific policy or constraint instance on default settings.
* TODO: consider making static.
* @method overlay
* @private
* @param {Object} base The default object
* @param {Object} overlay The superceeding object
* @returns {Object} A new object with base parameters when not set in overlay.
*/
Policy.prototype.overlay = function(base, overlay) {
var ret = {};
util.mixin(ret, base);
if (overlay) {
util.mixin(ret, overlay, true);
}
return ret;
};
module.exports = Policy;