/**
* Extension manager.
* @module Manager
*/
const { Graph, ERROR_DUPLICATED_EDGE, ERROR_NO_TOPO_ORDER } = require('./util/graph');
const { matchVersion } = require('./util/version');
const loadMode = {
UNLOAD: 0,
INITIATIVE_LOAD: 1,
PASSIVE_LOAD: 2
};
const ERROR_UNAVAILABLE_EXTENSION = 0x90;
const ERROR_CIRCULAR_REQUIREMENT = 0x91;
/**
* Extension manager.
*/
class ExtensionManager {
constructor() {
this.info = {};
this.instance = {};
this.load = {};
}
/**
* Add an extension.
* @param {string} id - Extension id.
* @param {ExtensionInfo} info - Extension info.
* @param {Extension} instance - Extension instance.
*/
addInstance(id, info, instance) {
if (this.instance.hasOwnProperty(id)) return;
this.info[id] = Object.assign({
dependency: {}
}, info);
this.load[id] = loadMode.UNLOAD;
this.instance[id] = instance;
}
/**
* Remove an extension.
* @param {string} id - Extension id.
*/
removeInstance(id) {
if (this.instance.hasOwnProperty(id)) {
delete this.info[id];
delete this.load[id];
delete this.instance[id];
}
}
/**
* Get an extension instance.
* @param {string} id - Extension id.
* @returns {Extension} - Extension instance.
*/
getInstance(id) {
return this.instance[id];
}
/**
* Get an extension info.
* @param {string} id - Extension id.
* @returns {ExtensionInfo} - Extension info.
*/
getInfo(id) {
console.log(this.info)
return this.info[id];
}
/**
* Check if an extension existed.
* @param {string} id - Extension id.
*/
exist(id) {
return this.instance.hasOwnProperty(id);
}
setLoadStatus(id, loadStatus) {
this.load[id] = loadStatus;
}
getLoadStatus(id) {
return this.load[id];
}
getLoadedExtensions() {
const result = [];
for (const key in this.load) {
if (this.load[key]) result.push(key);
}
return result;
}
/**
* Load all the extensions given.
* @param {Object[]} extensions - The list of extension ID.
* @param {Function} vmCallback - Load vm extension.
*/
loadExtensionsWithMode(extensions, vmCallback) {
for (const extension of extensions) {
if (!this.getLoadStatus(extension.id)) {
if (!this.info[extension.id].api) { // undefined, null or 0
vmCallback(extension.id);
}
else {
this.instance[extension.id].onInit();
}
this.setLoadStatus(extension.id, extension.mode);
}
}
}
/**
* Unload all the extensions given.
* @param {string[]} extensions - The list of extension ID.
*/
unloadExtensions(extensions) {
for (const extension of extensions) {
if (this.getLoadStatus(extension)) {
this.instance[extension].onUninit();
this.setLoadStatus(extension, loadMode.UNLOAD);
}
}
}
/**
* Get the correct loading order.
* @param {string[]} extensions - The list of extension ID.
* @returns {Object[]}
*/
getExtensionLoadOrder(extensions) {
const graph = new Graph();
for (const extensionId of extensions) {
if (!this.info.hasOwnProperty(extensionId)) {
console.error(`Unavailable extension: ${extensionId}`);
throw ERROR_UNAVAILABLE_EXTENSION;
}
this._checkExtensionLoadingOrderById(extensionId, [], graph);
}
return graph.topo().map(id => ({
id: id,
mode: extensions.includes(id) ? loadMode.INITIATIVE_LOAD : loadMode.PASSIVE_LOAD
}));
}
_findIdInList(id, list) {
for (const i in list) {
if (list[i].id == id) {
return i;
}
}
return -1;
}
/**
* Get loading order of the extension with given id.
* @param {string} extensionId - The extension id.
* @param {Object[]} requireStack - Require stack.
* @param {Graph} graph - Load order.
*/
_checkExtensionLoadingOrderById(extensionId, requireStack, graph) {
requireStack.push({
id: extensionId,
version: this.info[extensionId].version
});
if (!graph.hasNode(extensionId)) {
graph.addNode(extensionId);
}
for (const dependency in this.info[extensionId].dependency) {
if (!this.info.hasOwnProperty(dependency)) {
console.error(`Unavailable extension: ${dependency}`);
this._printRequireStack(requireStack);
throw ERROR_UNAVAILABLE_EXTENSION;
}
if (this._findIdInList(dependency, requireStack) >= 0) {
console.error(`Circular requirement: ${dependency}`);
this._printRequireStack(requireStack);
throw ERROR_CIRCULAR_REQUIREMENT;
}
const targetVersion = this.info[extensionId].dependency[dependency];
if (matchVersion(this.info[dependency].version, targetVersion)) {
graph.addEdge(dependency, extensionId);
this._checkExtensionLoadingOrderById(dependency, requireStack, graph);
/*if (!this._findIdInList(dependency, result)) {
result.unshift({ id: dependency, mode: loadMode.PASSIVE_LOAD });
this._checkExtensionLoadingOrderById(dependency, requireStack, result);
}*/
}
else {
console.error(`Unmatched version: ${extensionId}(${targetVersion}), got ${this.info[dependency].version}`);
this._printRequireStack(requireStack);
throw ERROR_UNAVAILABLE_EXTENSION;
}
}
requireStack.pop();
}
/**
* Get the correct unloading order.
* @param {string[]} extensions - The list of extension ID.
*/
getExtensionUnloadOrder(extensions) {
const graph = new Graph();
for (const extension of extensions) {
if (this.load.hasOwnProperty(extension) && this.load[extension]) {
this._checkExtensionUnloadingOrderById(extension, graph);
}
}
return graph.topo();
}
/**
* Get unloading order of the extension with given id.
* @param {string} extensionId - Extension ID.
* @param {Graph} graph - Unlaod order.
*/
_checkExtensionUnloadingOrderById(extensionId, graph, last) {
if (!graph.hasNode(extensionId)) {
graph.addNode(extensionId);
}
for (const extension in this.load) {
if (extension === last) continue;
if (this.load[extension]) {
if (this.info[extension].dependency.hasOwnProperty(extensionId)) {
graph.addEdge(extension, extensionId);
this._checkExtensionUnloadingOrderById(extension, graph, extensionId);
}
}
}
for (const dependency in this.info[extensionId].dependency) {
if (dependency === last) continue;
if (this.load[dependency] === loadMode.PASSIVE_LOAD) {
graph.addEdge(extensionId, dependency);
this._checkExtensionUnloadingOrderById(dependency, graph, extensionId);
}
}
}
_printRequireStack(requireStack) {
while (requireStack.length > 0) {
const item = requireStack.pop();
console.error(` required by ${item.id}(${item.version})`);
}
}
emitEventToExtension(id, event, ...args) {
if (!this.instance.hasOwnProperty(id)) throw `Unavaliable extension id: ${id}`;
const func = this.instance[id][event];
if (typeof func === 'function') {
func(...args);
}
}
emitEvent(event, ...args) {
for (const key in this.load) {
if (this.load[key]) {
const func = this.instance[key][event];
if (typeof func === 'function') {
func(...args);
}
}
}
}
}
const extensionManager = new ExtensionManager();
module.exports = {
ExtensionManager,
extensionManager,
ERROR_UNAVAILABLE_EXTENSION,
ERROR_CIRCULAR_REQUIREMENT
};