Skip to content

Instantly share code, notes, and snippets.

@IUnknown68
Last active August 29, 2015 14:27
Show Gist options
  • Save IUnknown68/df6fcdd44f17d5cb3095 to your computer and use it in GitHub Desktop.
Save IUnknown68/df6fcdd44f17d5cb3095 to your computer and use it in GitHub Desktop.
The DependencyManager handles asynchronous module initialization with dependencies.
//------------------------------------------------------------------------------
// DependencyManager
// The DependencyManager handles asynchronous module initialization with
// dependencies.
//
// Example:
// =======
// Modules A and B load asynchronously.
// Module C requires A and B and loads synchronously.
// Module D requires C and loads asynchronously.
//
// So in the first iteration (called runlevel) `load()` will be called on
// A and B. They return a promise since they work asynchronously.
//
// When both promises are resolved, the next runlevel will be executed, and
// `C.load()` will be called. Since C loads synchronously, it returns a falsy
// value.
//
// The next runlevel will be executed and load D. When D is loaded, the promise
// returned by `DependencyManager.init()` will resolve.
//
// Modules
// =======
// A Module must implement the following interface:
// name : required, string Name of the module.
// dependencies : optional, array Array with modulenames this module
// depends on.
// load(runLevel) : required, function Method called to ask the module to
// load. Only argument is `runLevel`.
// loaded : boolean This will be set by the manager when the module
// is loaded. Can initially be undefined.
//
// The return value of `load()` decides about further treatment of the module:
// Falsy: The module loaded successfully and `load()` will not get called
// again.
// True: `load()` should be called again on the next runlevel.
// A promise: The module will be marked as loaded when the promise gets
// resolved.
//
// Howto
// =====
// The DependencyManager has two methods:
// addModules(...) Takes any number or arguments, each being a module or an
// array of modules.
// This must be called before `init()`.
// init(runLevel) Loads the moduls up to `runLevel`. `runLevel` defaults
// to -1, which means run all the way up to the top.
// `init()` can be called muliple times, e.g. you init to level 1, let the user
// login, and then init the rest.
//------------------------------------------------------------------------------
(function(){
var _modules = {};
var _runLevel = 0;
var _manager = {};
//----------------------------------------------------------------------------
// walk through all runlevels up to `maxRunLevel` or until there are no
// outstanding modules.
function _initRunlevelTo(deferred, maxRunLevel) {
if ((-1 !== maxRunLevel) && (_runLevel > maxRunLevel)) {
deferred.resolve(_runLevel-1);
}
else {
try {
var result = _initCurrentRunlevel();
if (result) {
// result is a promise
result.then(function() {
_runLevel++;
_initRunlevelTo(deferred, maxRunLevel);
}).catch(deferred.reject);
}
else {
// result is false means no outstanding work. We are done.
deferred.resolve(_runLevel-1);
}
}
catch(e) {
deferred.reject(e);
}
}
return deferred.promise;
}
//----------------------------------------------------------------------------
// check and if possible execute each module's load function
function _initCurrentRunlevel() {
var promises = Object.keys(_modules).
map(function(moduleName) {
return _initModule(_modules[moduleName]);
}).
filter(function(result) {
return !!result;
});
return (promises.length) ? Promise.all(promises) : false;
}
//----------------------------------------------------------------------------
function _initModule(module) {
if (module.loaded) {
return false; // we are done
}
// check if all dependencies are loaded
var canLoad = (module.dependencies)
? module.dependencies.every(function(moduleName) {
if (!_modules[moduleName]) {
throw new Error('Module "' + moduleName + '" does not exist.');
}
return _modules[moduleName].loaded;
})
: true;
if (!canLoad) {
return true; // Unresolved dependencies: Try again on next runlevel.
}
// call load on module
var result = module.load(_runLevel);
if (!result) {
// If result is falsy mark as loaded to prevent further calling.
module.loaded = true;
}
if (('object' === typeof result) && ('function' === typeof result.then)) {
// A promise. Hook to set `loaded`.
var deferred = _defer();
result.then(function(module) {
module.loaded = true;
deferred.resolve(module);
}).catch(deferred.reject);
return deferred.promise;
}
// If result is not falsy the module will be called again in the next runlevel.
return !!result;
}
//----------------------------------------------------------------------------
// create a promise and return
// {
// promise: ...
// reject: ...
// resolve: ...
// }
// You might want to replace this with your favourite implementation.
function _defer() {
var deferred = {};
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
}
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
//--------------------------------------------------------------------------
// method addModules
// Takes any number or arguments, each being a module or an array
// of modules.
// Must be called before `init()`
_manager.addModules = function() {
for (var n = 0; n < arguments.length; n++) {
var module = arguments[n];
if (Array.isArray(module)) {
// arrays are parsed recursively
_manager.addModules.apply(_manager, module);
continue;
}
if ('object' !== typeof module) {
throw new Error('Module must be an object');
}
if (!module.name) {
throw new Error('Module must have a name');
}
if ('function' !== typeof module.load) {
throw new Error('Module must have a load method');
}
_modules[module.name] = module;
}
return this;
};
//--------------------------------------------------------------------------
// method init
// param `maxRunLevel` : stop when this runlevel is initialized.
_manager.init = function(maxRunLevel) {
maxRunLevel = parseInt(maxRunLevel, 10);
maxRunLevel = isNaN(maxRunLevel) ? -1 : maxRunLevel;
var deferred = _defer();
return _initRunlevelTo(deferred, maxRunLevel);
};
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
// set global object
var globalName = 'DependencyManager';
var scripts = document.getElementsByTagName('script');
scripts[scripts.length - 1].src.replace(/^[^\?]+\??/,'').split(/[;&]/).some(function(param) {
var ar = param.split('=');
if ('manager' === ar[0]) {
globalName = ar[1];
return true;
}
});
window[globalName] = _manager;
})();
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Sample</title>
</head>
<body>
<script src="DependencyManager.js?manager=theManager"></script>
<script>
//----------------------------------------------------------------------------
function _defer() {
var deferred = {};
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
}
//----------------------------------------------------------------------------
function fakeLoad(module) {
var deferred = _defer();
window.setTimeout(function() {
console.log(this.name, 'LOADED');
deferred.resolve(this);
}.bind(module), Math.random() * 2000 + 500);
return deferred.promise;
}
//----------------------------------------------------------------------------
// Note that the order of modules does not matter.
var modules = [
{
name: 'D',
dependencies: ['C'],
load: function(runLevel) {
console.log(this.name, 'load for level', runLevel);
// loading asynchronously, so return promise
return fakeLoad(this);
}
},
{
name: 'C',
dependencies: ['A','B'],
load: function(runLevel) {
console.log(this.name, 'load for level', runLevel);
// Loading synchronously, and we are done. So return nothing.
console.log(this.name, 'LOADED synchronously');
}
},
{
name: 'A',
load: function(runLevel) {
console.log(this.name, 'load for level', runLevel);
return fakeLoad(this);
}
},
{
name: 'B',
load: function(runLevel) {
console.log(this.name, 'load for level', runLevel);
return fakeLoad(this);
}
}
];
theManager.
addModules(modules).
init().
then(function(runLevel) {
console.info('Reached runlevel', runLevel);
}).
catch(function(e) {
console.warn(e);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment