ExtensionPoint async loading

This commit is contained in:
tfennelly 2016-03-01 20:21:58 +00:00
parent 411a844149
commit 31ecd20548
6 changed files with 234 additions and 5 deletions

View File

@ -4,8 +4,20 @@ var store = require('./store.js');
var ExtensionPoint = React.createClass({
getInitialState: function () {
// Initial state is the set of extensions.
return {
extensions: []
};
},
componentDidMount: function() {
this._renderAllExtensions();
var thisEp = this;
store.loadExtensions(this.props.name, function(extensions) {
thisEp.setState({
extensions: extensions
});
});
},
componentDidUpdate: function() {
@ -16,9 +28,12 @@ var ExtensionPoint = React.createClass({
this._unmountAllExtensions();
},
// TODO: resolve possible differences/inconsistencies between render, _renderAllExtensions and _renderExtension
// Something looks wrong with all of that... nested render, render, render
render: function() {
var extensionDivs = [];
var extensions = store.getExtensions(this.props.name);
var extensions = this.state.extensions;
for (var i = 0; i < extensions.length; i++) {
extensionDivs.push(<div key={i}/>);
@ -67,4 +82,4 @@ ExtensionPoint.propTypes = {
wrappingElement: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
};
module.exports = ExtensionPoint;
module.exports = ExtensionPoint;

View File

@ -0,0 +1,4 @@
//
// See https://github.com/jenkinsci/js-builder
//
require('jenkins-js-builder');

View File

@ -1,6 +1,6 @@
{
"name": "@jenkins-cd/js-extensions",
"version": "0.0.2",
"version": "0.0.3",
"description": "Jenkins Extension Store",
"main": "index.js",
"scripts": {
@ -15,6 +15,12 @@
"react-dom": "^0.14.7"
},
"devDependencies": {
"gulp": "^3.9.1",
"jenkins-js-builder": "0.0.51",
"jenkins-js-test": "^1.0.1",
"react-tools": "^0.13.3"
},
"dependencies": {
"jenkins-js-modules": "^1.5.4"
}
}

View File

@ -0,0 +1,36 @@
[
{
"extensions": [
{
"component": "component-1.1",
"extensionPoint": "ep-1"
},
{
"component": "component-1.2",
"extensionPoint": "ep-1"
},
{
"component": "component-1.3",
"extensionPoint": "ep-2"
}
],
"hpiPluginId": "plugin-1"
},
{
"extensions": [
{
"component": "component-2.1",
"extensionPoint": "ep-1"
},
{
"component": "component-2.2",
"extensionPoint": "ep-2"
},
{
"component": "component-1.3",
"extensionPoint": "ep-2"
}
],
"hpiPluginId": "plugin-2"
}
]

View File

@ -0,0 +1,56 @@
describe("store.js", function () {
it("- test ", function (done) {
var javaScriptExtensionInfo = require('./javaScriptExtensionInfo-01.json');
var store = require('../store');
var jsModules = require('jenkins-js-modules');
var pluginsLoaded = {};
var loaded = 0;
// Mock the calls to addScript
jsModules.addScript = function(scriptUrl, options) {
expect(scriptUrl).toBe('io/jenkins/' + options.hpiPluginId + '/jenkins-js-extension.js');
// mimic registering of those extensions
for(var i1 = 0; i1 < javaScriptExtensionInfo.length; i1++) {
var pluginMetadata = javaScriptExtensionInfo[i1];
var extensions = pluginMetadata.extensions;
for(var i2 = 0; i2 < extensions.length; i2++) {
store.addExtension(extensions[i2].component, extensions[i2].extensionPoint);
}
}
if (pluginsLoaded[options.hpiPluginId] === undefined) {
pluginsLoaded[options.hpiPluginId] = true;
loaded++;
}
options.success();
};
// Initialise the store with some extension point info. At runtime,
// this info will be loaded from <jenkins>/blue/javaScriptExtensionInfo
store.setExtensionPointMetadata(javaScriptExtensionInfo);
// Call load for ExtensionPoint impls 'ep-1'. This should mimic
// the store checking all plugins and loading the bundles for any
// plugins that define an impl of 'ep-1' (if not already loaded).
store.loadExtensions('ep-1', function() {
if (loaded === 2) {
expect(pluginsLoaded['plugin-1']).toBeDefined();
expect(pluginsLoaded['plugin-2']).toBeDefined();
// if we call load again, nothing should happen as
// all plugin bundles have been loaded i.e. loaded
// should still be 2 (i.e. unchanged).
store.loadExtensions('ep-1', function() {
expect(loaded, 2);
// Calling it yet again for different extension point, but
// where the bundles for that extension point have already.
store.loadExtensions('ep-2', function() {
expect(loaded, 2);
done();
});
});
}
});
});
});

View File

@ -1,4 +1,22 @@
/**
* The registered ExtensionPoint instances.
*/
var points = {};
/**
* The ExtensionPoint metadata.
*/
var extensionPointMetadata = [];
// TODO: Probably need some sort of mechanism to support refreshing of the ExtensionPoint info based on plugin install/uninstall.
// We need an eventbus of some sort.
exports.setExtensionPointMetadata = function(data) {
// This data should come from <jenkins>/blue/javaScriptExtensionInfo
if (data) {
// We clone the data because we add to it.
extensionPointMetadata = JSON.parse(JSON.stringify(data));
}
};
exports.addExtensionPoint = function(key) {
points[key] = points[key] || [];
@ -9,6 +27,100 @@ exports.addExtension = function (key, extension) {
points[key].push(extension);
};
exports.loadExtensions = function(key, onload) {
loadBundles(key, function() {
var point = points[key];
onload(point);
});
};
exports.getExtensions = function(key) {
return points[key] || [];
};
};
function LoadCountMonitor() {
this.counter = 0;
this.callback = undefined;
}
LoadCountMonitor.prototype.inc = function() {
this.counter++;
if (this.callback) {
this.callback();
}
};
LoadCountMonitor.prototype.dec = function() {
this.counter--;
if (this.callback) {
this.callback();
}
};
LoadCountMonitor.prototype.onchange = function(callback) {
this.callback = callback;
};
function loadBundles(extensionPointId, onBundlesLoaded) {
var jsModules = require('jenkins-js-modules');
var loadCountMonitor = new LoadCountMonitor();
function loadPluginBundle(pluginMetadata) {
var scriptUrl = 'io/jenkins/' + pluginMetadata.hpiPluginId + '/jenkins-js-extension.js';
loadCountMonitor.inc();
// The plugin bundle for this plugin may already be in the process of loading (async extension
// point rendering). If it's not, pluginMetadata.loadCountMonitors will not be undefined,
// which means we can go ahead with the async loading. If it is, pluginMetadata.loadCountMonitors
// is defined, we just add "this" loadCountMonitor to pluginMetadata.loadCountMonitors.
// It will get called as soon as the script loading is complete.
if (!pluginMetadata.loadCountMonitors) {
pluginMetadata.loadCountMonitors = [];
pluginMetadata.loadCountMonitors.push(loadCountMonitor);
jsModules.addScript(scriptUrl, {
scriptSrcBase: '@adjunct',
hpiPluginId: pluginMetadata.hpiPluginId, // Used for testing
success: function () {
pluginMetadata.bundleLoaded = true;
for (var i = 0; i < pluginMetadata.loadCountMonitors.length; i++) {
pluginMetadata.loadCountMonitors[i].dec();
}
}
});
} else {
pluginMetadata.loadCountMonitors.push(loadCountMonitor);
}
}
function checkLoading() {
if (loadCountMonitor.counter === 0) {
onBundlesLoaded();
}
}
// Iterate over each plugin in extensionPointMetadata, async loading
// the extension point .js bundle (if not already loaded) for each of the
// plugins that implement the specified extensionPointId.
for(var i1 = 0; i1 < extensionPointMetadata.length; i1++) {
var pluginMetadata = extensionPointMetadata[i1];
var extensions = pluginMetadata.extensions;
for(var i2 = 0; i2 < extensions.length; i2++) {
if (extensions[i2].extensionPoint === extensionPointId) {
// This plugin implements the ExtensionPoint.
// If we haven't already loaded the extension point
// bundle for this plugin, lets load it now.
if (!pluginMetadata.bundleLoaded) {
loadPluginBundle(pluginMetadata);
}
}
}
}
// Listen to the inc/dec calls now that we've iterated
// over all of the plugins.
loadCountMonitor.onchange(function() {
checkLoading();
});
// Call checkLoading immediately in case all plugin
// bundles have been loaded already.
checkLoading();
}