Files
UXP-Fixed/devtools/shared/gcli/source/lib/gcli/system.js
T
2018-02-02 04:16:08 -05:00

371 lines
11 KiB
JavaScript

/*
* Copyright 2012, Mozilla Foundation and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var util = require('./util/util');
var Commands = require('./commands/commands').Commands;
var Connectors = require('./connectors/connectors').Connectors;
var Converters = require('./converters/converters').Converters;
var Fields = require('./fields/fields').Fields;
var Languages = require('./languages/languages').Languages;
var Settings = require('./settings').Settings;
var Types = require('./types/types').Types;
/**
* This is the heart of the API that we expose to the outside.
* @param options Object that customizes how the system acts. Valid properties:
* - commands, connectors, converters, fields, languages, settings, types:
* Custom configured manager objects for these item types
* - location: a system with a location will ignore commands that don't have a
* matching runAt property. This is principly for client/server setups where
* we import commands from the server to the client, so a system with
* `{ location: 'client' }` will silently ignore commands with
* `{ runAt: 'server' }`. Any system without a location will accept commands
* with any runAt property (including none).
*/
exports.createSystem = function(options) {
options = options || {};
var location = options.location;
// The plural/singular thing may make you want to scream, but it allows us
// to say components[getItemType(item)], so a lookup here (and below) saves
// multiple lookups in the middle of the code
var components = {
connector: options.connectors || new Connectors(),
converter: options.converters || new Converters(),
field: options.fields || new Fields(),
language: options.languages || new Languages(),
type: options.types || new Types()
};
components.setting = new Settings(components.type);
components.command = new Commands(components.type, location);
var getItemType = function(item) {
if (item.item) {
return item.item;
}
// Some items are registered using the constructor so we need to check
// the prototype for the the type of the item
return (item.prototype && item.prototype.item) ?
item.prototype.item : 'command';
};
var addItem = function(item) {
try {
components[getItemType(item)].add(item);
}
catch (ex) {
if (item != null) {
console.error('While adding: ' + item.name);
}
throw ex;
}
};
var removeItem = function(item) {
components[getItemType(item)].remove(item);
};
/**
* loadableModules is a lookup of names to module loader functions (like
* the venerable 'require') to which we can pass a name and get back a
* JS object (or a promise of a JS object). This allows us to have custom
* loaders to get stuff from the filesystem etc.
*/
var loadableModules = {};
/**
* loadedModules is a lookup by name of the things returned by the functions
* in loadableModules so we can track what we need to unload / reload.
*/
var loadedModules = {};
var unloadModule = function(name) {
var existingModule = loadedModules[name];
if (existingModule != null) {
existingModule.items.forEach(removeItem);
}
delete loadedModules[name];
};
var loadModule = function(name) {
var existingModule = loadedModules[name];
unloadModule(name);
// And load the new items
try {
var loader = loadableModules[name];
return Promise.resolve(loader(name)).then(function(newModule) {
if (existingModule === newModule) {
return;
}
if (newModule == null) {
throw 'Module \'' + name + '\' not found';
}
if (newModule.items == null || typeof newModule.items.forEach !== 'function') {
console.log('Exported properties: ' + Object.keys(newModule).join(', '));
throw 'Module \'' + name + '\' has no \'items\' array export';
}
newModule.items.forEach(addItem);
loadedModules[name] = newModule;
});
}
catch (ex) {
console.error('Failed to load module ' + name + ': ' + ex);
console.error(ex.stack);
return Promise.resolve();
}
};
var pendingChanges = false;
var system = {
addItems: function(items) {
items.forEach(addItem);
},
removeItems: function(items) {
items.forEach(removeItem);
},
addItemsByModule: function(names, options) {
var promises = [];
options = options || {};
if (!options.delayedLoad) {
// We could be about to add many commands, just report the change once
this.commands.onCommandsChange.holdFire();
}
if (typeof names === 'string') {
names = [ names ];
}
names.forEach(function(name) {
if (options.loader == null) {
options.loader = function(name) {
return require(name);
};
}
loadableModules[name] = options.loader;
if (options.delayedLoad) {
pendingChanges = true;
}
else {
promises.push(loadModule(name).catch(console.error));
}
});
if (options.delayedLoad) {
return Promise.resolve();
}
else {
return Promise.all(promises).then(function() {
this.commands.onCommandsChange.resumeFire();
}.bind(this));
}
},
removeItemsByModule: function(name) {
this.commands.onCommandsChange.holdFire();
delete loadableModules[name];
unloadModule(name);
this.commands.onCommandsChange.resumeFire();
},
load: function() {
if (!pendingChanges) {
return Promise.resolve();
}
this.commands.onCommandsChange.holdFire();
// clone loadedModules, so we can remove what is left at the end
var modules = Object.keys(loadedModules).map(function(name) {
return loadedModules[name];
});
var promises = Object.keys(loadableModules).map(function(name) {
delete modules[name];
return loadModule(name).catch(console.error);
});
Object.keys(modules).forEach(unloadModule);
pendingChanges = false;
return Promise.all(promises).then(function() {
this.commands.onCommandsChange.resumeFire();
}.bind(this));
},
destroy: function() {
this.commands.onCommandsChange.holdFire();
Object.keys(loadedModules).forEach(function(name) {
unloadModule(name);
});
this.commands.onCommandsChange.resumeFire();
},
toString: function() {
return 'System [' +
'commands:' + components.command.getAll().length + ', ' +
'connectors:' + components.connector.getAll().length + ', ' +
'converters:' + components.converter.getAll().length + ', ' +
'fields:' + components.field.getAll().length + ', ' +
'settings:' + components.setting.getAll().length + ', ' +
'types:' + components.type.getTypeNames().length + ']';
}
};
Object.defineProperty(system, 'commands', {
get: function() { return components.command; },
enumerable: true
});
Object.defineProperty(system, 'connectors', {
get: function() { return components.connector; },
enumerable: true
});
Object.defineProperty(system, 'converters', {
get: function() { return components.converter; },
enumerable: true
});
Object.defineProperty(system, 'fields', {
get: function() { return components.field; },
enumerable: true
});
Object.defineProperty(system, 'languages', {
get: function() { return components.language; },
enumerable: true
});
Object.defineProperty(system, 'settings', {
get: function() { return components.setting; },
enumerable: true
});
Object.defineProperty(system, 'types', {
get: function() { return components.type; },
enumerable: true
});
return system;
};
/**
* Connect a local system with another at the other end of a connector
* @param system System to which we're adding commands
* @param front Front which allows access to the remote system from which we
* import commands
* @param customProps Array of strings specifying additional properties defined
* on remote commands that should be considered part of the metadata for the
* commands imported into the local system
*/
exports.connectFront = function(system, front, customProps) {
system._handleCommandsChanged = function() {
syncItems(system, front, customProps).catch(util.errorHandler);
};
front.on('commands-changed', system._handleCommandsChanged);
return syncItems(system, front, customProps);
};
/**
* Undo the effect of #connectFront
*/
exports.disconnectFront = function(system, front) {
front.off('commands-changed', system._handleCommandsChanged);
system._handleCommandsChanged = undefined;
removeItemsFromFront(system, front);
};
/**
* Remove the items in this system that came from a previous sync action, and
* re-add them. See connectFront() for explanation of properties
*/
function syncItems(system, front, customProps) {
return front.specs(customProps).then(function(specs) {
removeItemsFromFront(system, front);
var remoteItems = addLocalFunctions(specs, front);
system.addItems(remoteItems);
return system;
});
};
/**
* Take the data from the 'specs' command (or the 'commands-changed' event) and
* add function to proxy the execution back over the front
*/
function addLocalFunctions(specs, front) {
// Inject an 'exec' function into the commands, and the front into
// all the remote types
specs.forEach(function(commandSpec) {
// HACK: Tack the front to the command so we know how to remove it
// in removeItemsFromFront() below
commandSpec.front = front;
// Tell the type instances for a command how to contact their counterparts
// Don't confuse this with setting the front on the commandSpec which is
// about associating a proxied command with it's source for later removal.
// This is actually going to be used by the type
commandSpec.params.forEach(function(param) {
if (typeof param.type !== 'string') {
param.type.front = front;
}
});
if (!commandSpec.isParent) {
commandSpec.exec = function(args, context) {
var typed = (context.prefix ? context.prefix + ' ' : '') + context.typed;
return front.execute(typed).then(function(reply) {
var typedData = context.typedData(reply.type, reply.data);
return reply.isError ? Promise.reject(typedData) : typedData;
});
};
}
commandSpec.isProxy = true;
});
return specs;
}
/**
* Go through all the commands removing any that are associated with the
* given front. The method of association is the hack in addLocalFunctions.
*/
function removeItemsFromFront(system, front) {
system.commands.getAll().forEach(function(command) {
if (command.front === front) {
system.commands.remove(command);
}
});
}