Last active
August 29, 2015 14:27
-
-
Save IUnknown68/df6fcdd44f17d5cb3095 to your computer and use it in GitHub Desktop.
The DependencyManager handles asynchronous module initialization with dependencies.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//------------------------------------------------------------------------------ | |
// 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; | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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