unclean SPARC
This commit is contained in:
Executable
+566
@@ -0,0 +1,566 @@
|
||||
'use strict'
|
||||
/**
|
||||
___ ____ __ ____ ___
|
||||
/ __)( _ \ /__\ ( _ \ / __)
|
||||
___\__ \ )___//(__)\ ) /( (__
|
||||
(_______/(__) (__)(__)(_)\_) \___) SPA Rational Code
|
||||
By Mike & Nike
|
||||
|
||||
This file is part of Sparc by Mike & Nike.
|
||||
Sparc is free software:
|
||||
- you can redistribute it and/or modify it under the terms of the GNU General Public License,
|
||||
as published by the Free Software Foundation, either version 3 of the License,
|
||||
or (at your option) any later version.
|
||||
- Sparc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
See the GNU General Public License for more details.
|
||||
Get your copy of the GNU General Public License at <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
* @category Core
|
||||
* @hideconstructor
|
||||
* @tutorial app-01-overview
|
||||
*/
|
||||
class Sparc {
|
||||
/**
|
||||
* Genaral app configuration, as soon as it is loaded
|
||||
* @tutorial app-02-configuration
|
||||
* */
|
||||
config = {};
|
||||
/** Classes definitions for MVC + Sparc libs follow the rule script-name = instantiable class name */
|
||||
LoadedClasses = {};
|
||||
/** Thirdparties and other libs don't necessarily have a class, or this naming conv. then store path only. */
|
||||
LoadedScripts = [];
|
||||
/** @type User */
|
||||
User = null
|
||||
/** @type Assets */
|
||||
Assets = null
|
||||
/** @type Router */
|
||||
Router = null
|
||||
/** @type Events */
|
||||
events = null
|
||||
/** @type Array<Object> */
|
||||
latestErrors = [];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} ref
|
||||
* @param {*} cls
|
||||
* @param {*} replace
|
||||
*/
|
||||
registerClass(ref, cls, replace=false) {
|
||||
if(replace || (!this.LoadedClasses.hasOwnProperty(ref))){
|
||||
this.LoadedClasses[ref] = cls;
|
||||
} else {
|
||||
console.warn('Attempting to register already defined class ', ref);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onBootReady() {
|
||||
console.log('Sparc boot loaded...');
|
||||
|
||||
// Instances shortcuts
|
||||
this.User = new this.LoadedClasses.User(); // This is User base class (no login)
|
||||
|
||||
// Static classes shortcuts
|
||||
this.Assets = Assets;
|
||||
|
||||
// General app config are loaded here
|
||||
this.Assets.loadJson({ 'name': 'config.json',
|
||||
'path': '/app/config/',
|
||||
}).then(this.onConfigLoaded.bind(this));
|
||||
// Instances shortcuts
|
||||
this.events = new this.LoadedClasses.Events(); // This is User base class (no login)
|
||||
this.addEvent = this.events.addEvent.bind(this.events);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onWindowReady() {
|
||||
// MVC not yet loaded here, see remark below about loading base classes.
|
||||
Loader.loadScripts({ 'basepath':'/core/libs/',
|
||||
'scripts': ['Router', 'Logger', 'Assets','Events'],
|
||||
'dependencies':{ 'Router': ['../baseClasses/User'] },
|
||||
}).then(this.onBootReady.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} config
|
||||
*/
|
||||
onConfigLoaded(config){
|
||||
console.log('App config loaded...');
|
||||
if(!config) {
|
||||
console.error('Could not load SPARC config file !?');
|
||||
return;
|
||||
}
|
||||
this.config = config;
|
||||
if(('logger' in config) && ('enabled' in config.logger) && (config.logger.enabled)
|
||||
&& ('levels' in config.logger) && ('postUrl' in config.logger)){
|
||||
window.console = this.logger(window.console, config.logger);
|
||||
}
|
||||
|
||||
this.Assets.defaults = config.assets;
|
||||
|
||||
if( ('userLib' in config) && ('className' in config.userLib) && (config.userLib.className!='')) {
|
||||
Loader.loadScripts({ 'basepath':'/app/libs/',
|
||||
'scripts': [config.userLib.className],
|
||||
'dependencies':{ }, // Dependencies of the User class from config ?
|
||||
}).then(()=>{
|
||||
this.User = new this.LoadedClasses.User(); // Re-instantiate extnded class
|
||||
// Upon creation, we are ourselves, but it might change later. (no OTS yet) Philosophical, isn't it :D
|
||||
this.currentUser = this.User;
|
||||
this.onUserClassReady();
|
||||
});
|
||||
} else {
|
||||
this.onUserClassReady();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onUserClassReady(){
|
||||
this.User.checkAuthenticated(()=>{
|
||||
if(this.User.isAuthenticated) {
|
||||
window.onbeforeunload = () => "Do you really want to leave EISMEA application ?";
|
||||
|
||||
// Loading base Classes, who can now use app.config.
|
||||
// If later we do multi-app then one config per app, and baseclass instantiated with its own config.
|
||||
Loader.loadScripts({
|
||||
'basepath':'/core/baseClasses/',
|
||||
'scripts': [
|
||||
'Model',
|
||||
'View',
|
||||
'MasterController'
|
||||
], // Look momy: sounds just like MVC ;-)
|
||||
'dependencies': {
|
||||
'MasterController': ['Controller']
|
||||
}, // Dependencies of the base classes ?
|
||||
}).then(this.loadLibraries.bind(this));
|
||||
|
||||
// Now load & start messageBus (must be authenticated, but can by done aside of loading the MVC core)
|
||||
if((this.config.messageBus.enabled) && ("Worker" in window) && ('WebSocket' in window)) {
|
||||
Loader.loadScripts({ 'basepath':'/core/libs/',
|
||||
'scripts': ['MessageBus'],
|
||||
}).then(() => {
|
||||
this.MessageBus = new this.LoadedClasses.MessageBus(this.config.messageBus, this.User.getMessageBusUserInfo());
|
||||
});
|
||||
} else if(this.config.messageBus.enabled){
|
||||
console.warn("Could not register MessageBus worker: Browser too old ?");
|
||||
}
|
||||
} else {
|
||||
console.log('User is not Authenticated, redirect to Login page');
|
||||
this.User.gotoLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
loadLibraries(){
|
||||
// Loading custom libs & intermediary classes
|
||||
let allPromises = [];
|
||||
let lib, libclass;
|
||||
if('libsBaseClasses' in this.config.router){
|
||||
for(lib of this.config.router.libsBaseClasses) {
|
||||
if(Array.isArray(lib.onlyIfClasses) && !lib.onlyIfClasses.every(cls => (cls in this.LoadedClasses))) continue
|
||||
libclass = lib.classes[0]; // Let all assets be dependencies of the first script (if many)
|
||||
Loader.AssetsDependencies[libclass] = lib.assets;
|
||||
allPromises.push( Loader.loadScripts({
|
||||
'basepath':'/app/libs'+lib.path,
|
||||
'scripts': lib.classes,
|
||||
'dependencies': lib.dependencies,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Promise.allSettled(allPromises).then(this.onMVCReady.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onMVCReady() {
|
||||
// Now that we are authenticated, we have the config and the MVC
|
||||
// load the top routes and then route !
|
||||
|
||||
this.Assets.loadJson({ 'name': 'baseRoutes.json', 'path': '/app/config/' })
|
||||
.then(
|
||||
(topRoutes) =>{
|
||||
if(!this.Router) {
|
||||
//console.log('Top routes loaded...', app.config);
|
||||
var routerOptions = {
|
||||
'defaults' : this.config.router,
|
||||
'routes': topRoutes,
|
||||
'callback': () =>{ } // Called when *routed* (not when instanciated)
|
||||
};
|
||||
if('getRolesFrom' in this.config.router){ //Role can be an array of string or string representing a function
|
||||
var isCallable = Function([],"return(typeof("+this.config.router.getRolesFrom+")=='function');")
|
||||
if(isCallable()) routerOptions['roles'] = Function([],"return("+this.config.router.getRolesFrom+"());");
|
||||
else routerOptions['roles'] = this.config.router.getRolesFrom;
|
||||
} else routerOptions['roles']='';
|
||||
this.Router = new Router(routerOptions);
|
||||
}
|
||||
this.Router.route();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Let the browser cache boot loader components or not ? (true in prod or if you don't work on core)
|
||||
const bootBrowserCache = false;
|
||||
|
||||
/* Here is Sparc bootstrap, depends only on Loader core class defined below */
|
||||
if(typeof(app)=='undefined') var app = new Sparc();
|
||||
|
||||
window.onload = app.onWindowReady.bind(app);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
___ _____ __ ____ ____ ____
|
||||
( ) ( _ ) /__\ ( _ \( ___)( _ \
|
||||
) (_ )(_)( /(__)\ )(_) ))__) ) /
|
||||
(____)(_____)(__)(__)(____/(____)(_)\_) for SPARC
|
||||
By Mike & Nike
|
||||
This static class contains the mechanics for loading javascripts. <br>
|
||||
It will load any needed dependancies for you (with the help of the dependancies file) <br>
|
||||
It can be called just boot-load you application and all its dependancies , <br>
|
||||
and/or, alternatively, you can load widgets yourself anytime.
|
||||
|
||||
* @static
|
||||
* @hideconstructor
|
||||
* @category Core
|
||||
* @todo found some direct reference to the app global var which is a bit sloppy
|
||||
*/
|
||||
class Loader {
|
||||
static Dependencies = {};
|
||||
static LoadedDepFiles = [];
|
||||
static AssetsDependencies = {};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static resolveDeps(imps, deps){
|
||||
const leveler = (levels, root, l, trail=[]) => {
|
||||
if(deps.hasOwnProperty(root)){
|
||||
for(var dep of deps[root]){
|
||||
if(!levels.hasOwnProperty(dep)) levels[dep] = -1;
|
||||
if(levels[dep]<l) levels[dep]=l;
|
||||
if(trail.indexOf(dep)<0){ // detect circular dependency to avoid infinite recursion
|
||||
let curtrail = [...trail];
|
||||
trail.push(dep);
|
||||
leveler(levels, dep , l+1, trail);
|
||||
trail = curtrail;
|
||||
} else console.error('Circular depencency with '+dep, trail);
|
||||
}
|
||||
}
|
||||
}
|
||||
var levels={};
|
||||
// Build the dep tree of things we need, noting the depth.
|
||||
for(var imp of imps) { leveler(levels, imp, 1); } // Level starts at 1, zero reserved for roots
|
||||
// Add requested imports as last, unless already with higher priority (tree shaking)
|
||||
for(var imp of imps) { if(!levels.hasOwnProperty(imp)) levels[imp] = 0; }
|
||||
// Group by levels, as their loading can be paralellized (whereas on level must wait on the prev. one)
|
||||
var name, level; var byLevel = {};
|
||||
for([name, level] of Object.entries(levels)) {
|
||||
if(!byLevel.hasOwnProperty(level)) { byLevel[level]=[]; }
|
||||
byLevel[level].push(name);
|
||||
}
|
||||
return(byLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static Import(scriptNames, deps, importIdPrfx, basePath = './'){ // Will start with real import (_import) only when / if dependencies are loaded.
|
||||
var CurrentImportChain = {};
|
||||
if(typeof(deps)=='string') { // deps is a path to a json file containing the dep tree
|
||||
if( (deps!='') && (Loader.LoadedDepFiles.indexOf(deps)<0) ){
|
||||
Loader.DependenciesLoader = new Promise((resolve, reject) => {
|
||||
fetch(basePath+deps, { headers: { "Content-Type": "application/json; charset=utf-8" }})
|
||||
.then(res => res.json())
|
||||
.then(response => {
|
||||
Loader.Dependencies = { ...Loader.Dependencies, ...response}; // Merge with known deps
|
||||
Loader.LoadedDepFiles.push(deps);
|
||||
resolve();
|
||||
})
|
||||
.catch(err => { console.error('Error loading dependencies'); Loader.Dependencies = {}; reject(); });
|
||||
});
|
||||
} else {
|
||||
Loader.DependenciesLoader = new Promise((resolve, reject) => { resolve(); }); //Already loaded, nothing to do here
|
||||
}
|
||||
} else if(typeof(deps)=='object') { // deps is the dep-tree object
|
||||
Loader.DependenciesLoader = new Promise((resolve, reject) => {
|
||||
Loader.Dependencies = { ...Loader.Dependencies, ...deps }; // Merge with known deps
|
||||
resolve(); // already ready ! ;-)
|
||||
});
|
||||
} else { console.error('Bad dependencies !?'); return(false); }
|
||||
// Prepare a synchronous chain of asynchronous loads
|
||||
Loader.DependenciesLoader.then(()=>{
|
||||
var byLevelImps = Loader.resolveDeps(scriptNames, Loader.Dependencies);
|
||||
var total = Object.values(byLevelImps).reduce((tot,v)=> (tot+v.length), 0);
|
||||
document.dispatchEvent(new CustomEvent("LoaderProgressAddTodo", {'detail': { 'importID':importIdPrfx, 'addValue': total }}));
|
||||
for(var key in Object.getOwnPropertyNames(byLevelImps).sort().reverse()){
|
||||
CurrentImportChain[importIdPrfx+'_'+key] = {'imps': byLevelImps[key], 'path':basePath };
|
||||
}
|
||||
var firstImportId = importIdPrfx+'_'+key;
|
||||
document.addEventListener('LoaderAllReady', function(evt) { // Next async imports in the sync chain
|
||||
var nextImpNb = evt.detail.substr(0,evt.detail.lastIndexOf('_')+1)+(parseInt(evt.detail.substr(evt.detail.lastIndexOf('_')+1))-1);
|
||||
if(evt.detail != importIdPrfx+'_0'){
|
||||
if(CurrentImportChain.hasOwnProperty(nextImpNb)) {
|
||||
Loader._import(CurrentImportChain[nextImpNb]['imps'], nextImpNb,CurrentImportChain[nextImpNb]['path']);
|
||||
}
|
||||
}
|
||||
});
|
||||
// First load all assets
|
||||
Loader.LoadAssetDependencies(byLevelImps).then( () => {
|
||||
// Start the synchronous chain
|
||||
Loader._import(CurrentImportChain[firstImportId]['imps'], firstImportId,CurrentImportChain[firstImportId]['path']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} byLevelImps
|
||||
* @returns {Array<Promise>}
|
||||
*/
|
||||
static LoadAssetDependencies(byLevelImps){
|
||||
let allImports = [];
|
||||
for(var level in byLevelImps) allImports=[...allImports, ...byLevelImps[level]];
|
||||
allImports = Array.from(new Set(allImports)); //remove dups
|
||||
allImports = allImports.map(this.path2ClassName);
|
||||
let allPromises = [];
|
||||
for(var classname of allImports){
|
||||
for(var assetType in Loader.AssetsDependencies[classname]){
|
||||
if(!Array.isArray(Loader.AssetsDependencies[classname][assetType])) {
|
||||
console.warn(`Bad config: Asset dependencies for ${classname} - ${assetType} is not an array !?`)
|
||||
continue;
|
||||
}
|
||||
for(var assetObj of Loader.AssetsDependencies[classname][assetType]){
|
||||
if((typeof(assetObj) != 'object') || (!('name' in assetObj))) {
|
||||
console.warn(`Bad config: One asset dependency for ${classname} - ${assetType} has no name !?`)
|
||||
continue;
|
||||
}
|
||||
switch(assetType){
|
||||
case 'fonts': allPromises.push(app.Assets.loadFont(assetObj));
|
||||
break;
|
||||
case 'styles': allPromises.push(app.Assets.loadCss(assetObj));
|
||||
break;
|
||||
case 'html': allPromises.push(app.Assets.loadHtml(assetObj));
|
||||
break;
|
||||
case 'views': assetObj.path = '/app/views/';
|
||||
allPromises.push(app.Assets.loadHtml(assetObj));
|
||||
break;
|
||||
case 'images': allPromises.push(app.Assets.getImage(assetObj));
|
||||
break;
|
||||
case 'json': allPromises.push(app.Assets.loadJson(assetObj));
|
||||
break;
|
||||
case 'sfx': allPromises.push(app.Assets.loadSound(assetObj));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return(Promise.allSettled(allPromises));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} src
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
script.src = src
|
||||
document.head.append(script)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static path2ClassName(scriptName){
|
||||
scriptName = scriptName.substr(scriptName.lastIndexOf('/')+1);
|
||||
if(scriptName.indexOf('.')==-1) scriptName+='.'; // also pure scriptname or path without extension compatible !
|
||||
return(scriptName.substr(0,scriptName.lastIndexOf('.')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static url2Path(url){
|
||||
var x = new URL(url);
|
||||
return(x.pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static _import(names, importID, basePath){ // Here, all imports are made asynchronously, in parallel.
|
||||
var allLoaded = true;
|
||||
var nbLeft = names.length;
|
||||
for(var name of names){
|
||||
var className = this.path2ClassName(name);
|
||||
if(name.substring(0,4)!='http') var scriptFp = (basePath + '/' + name).replace(/\/+/g,'/')+'.js';
|
||||
else var scriptFp = name;
|
||||
if( (!(className in app.LoadedClasses)) && (app.LoadedScripts.indexOf(scriptFp)<0) ) {
|
||||
allLoaded = false;
|
||||
if( !bootBrowserCache ) scriptFp += '?'+importID;
|
||||
Loader.loadScript(scriptFp)
|
||||
.then((result) => {
|
||||
var scriptName= this.url2Path(result.currentTarget.src);
|
||||
// MCV classes and internal ones put themselves in app.loadedClasses,
|
||||
// Non-Sparc scripts don't so save their path.
|
||||
if( ('router' in app.config) &&
|
||||
!(scriptName.startsWith(app.config.router.controllersPath)) &&
|
||||
!(scriptName.startsWith(app.config.router.modelsPath)) &&
|
||||
!(scriptName.startsWith(app.config.router.viewsPath)) &&
|
||||
!(scriptName.startsWith('/core/'))
|
||||
) app.LoadedScripts.push(scriptName);
|
||||
var className = this.path2ClassName(result.currentTarget.src);
|
||||
document.dispatchEvent(new CustomEvent('Loader'+className+'Ready', { 'detail': importID}));
|
||||
document.dispatchEvent(new CustomEvent("LoaderProgressIncrement", {'detail': { 'importID': importID, 'success': true }}));
|
||||
nbLeft--;
|
||||
if(nbLeft<=0){
|
||||
document.dispatchEvent(new CustomEvent("LoaderAllReady", { 'detail': importID }));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if((typeof(error)!='undefined') && (typeof(error.currentTarget)!='undefined')){
|
||||
var scriptName= error.currentTarget.src;
|
||||
var className = this.path2ClassName(error.currentTarget.src);
|
||||
console.error('Could not import Class '+className);
|
||||
document.dispatchEvent(new CustomEvent("LoaderError", { 'detail': {'importID': importID, 'className': className }}));
|
||||
} else console.error('Script loading error '+error);
|
||||
document.dispatchEvent(new CustomEvent("LoaderProgressIncrement", {'detail': { 'importID': importID, 'success': false }}));
|
||||
nbLeft--;
|
||||
});
|
||||
} else nbLeft--;
|
||||
}
|
||||
if(allLoaded){
|
||||
for(var name of names) document.dispatchEvent(new CustomEvent('Class'+name+'Ready', { 'detail':importID }));
|
||||
document.dispatchEvent(new CustomEvent("LoaderAllReady", { 'detail':importID }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a list of scripts.
|
||||
* @static
|
||||
* @param {string} basepath - base path (something like /sccripts/)
|
||||
* @param {Array<string>} scripts - array of scripts to load
|
||||
* @param {(string|object)} dependencies - Last minute Dependancies :tree object ( like {'scriptName': ['deps1', 'dep2,...]} ) or path to its json file.
|
||||
* @param {function} callback - Called when everything is loaded
|
||||
* @returns {Promise} A promise resolved whn-en all is loaded
|
||||
*/
|
||||
static loadScripts(args){
|
||||
// Start by filtering out scripts we already have
|
||||
if(!args.refresh) {
|
||||
args.scripts = args.scripts.filter((sname)=>{
|
||||
var className = this.path2ClassName(sname);
|
||||
return(!(className in app.LoadedClasses) && (!(sname in app.LoadedScripts)));
|
||||
});
|
||||
if(args.scripts.length==0) {
|
||||
if( (args.hasOwnProperty('callback')) && (typeof(args.callback)=='function') ){
|
||||
args.callback({});
|
||||
}
|
||||
return(new Promise((resolve) => { resolve(); }));
|
||||
}
|
||||
}
|
||||
var myImportID = crypto.randomUUID();
|
||||
if(!args.hasOwnProperty('dependencies')) args.dependencies = [];
|
||||
if(!args.hasOwnProperty('basepath')) args.basepath = '';
|
||||
return(new Promise((resolve, fail) => {
|
||||
document.addEventListener('LoaderAllReady', (evt) => { if(evt.detail==myImportID+'_0') resolve(); } );
|
||||
document.addEventListener('LoaderError', (evt) => { if(evt.detail==myImportID+'_0') fail(); } );
|
||||
Loader.Import(args.scripts, args.dependencies, myImportID, args.basepath);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a list of views.
|
||||
* @static
|
||||
* @param {array} Views - array of Views to load
|
||||
* @returns {string} The unique ID of the import
|
||||
*/
|
||||
static async loadViews(views){
|
||||
// Start with views templates
|
||||
let allPromises = []
|
||||
let templates = []
|
||||
let scripts = []
|
||||
let dependencies = {}
|
||||
for(var dep of views){
|
||||
if(typeof(dep) == 'string') {// just add view as dependency of controller, with correct path
|
||||
templates.push(dep+'.html')
|
||||
scripts.push(dep)
|
||||
} else if(typeof(dep) == 'object'){ // This view depends on other views
|
||||
scripts.push(dep.view);
|
||||
dependencies[dep.view]=dep.dependencies//.map(x=>'/app/views/'+x)
|
||||
templates.push(dep.view+'.html')
|
||||
}
|
||||
}
|
||||
|
||||
for(let view of templates){
|
||||
let assetObj = { path: '/app/views/', name: view }
|
||||
allPromises.push(app.Assets.loadHtml(assetObj));
|
||||
}
|
||||
await Promise.allSettled(allPromises);
|
||||
|
||||
// Finish with views scripts
|
||||
return(this.loadScripts({
|
||||
basepath:'/app/views/',
|
||||
scripts: scripts,
|
||||
dependencies: dependencies,
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* POLYFILLS
|
||||
*/
|
||||
|
||||
if(typeof(crypto.randomUUID)!='function'){
|
||||
crypto.randomUUID = () => {
|
||||
var buf = new Uint8Array(14);
|
||||
crypto.getRandomValues(buf);
|
||||
var uuid = Array.from(buf, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
||||
return(
|
||||
uuid.substring(0,8) + '-' +
|
||||
uuid.substring(10,4) + '-' +
|
||||
uuid.substring(14,4) + '-' +
|
||||
uuid.substring(18,4) + '-' +
|
||||
uuid.substring(22)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof(typeof(Promise.allSettled))!='function'){
|
||||
Promise.allSettled = ((promises) => Promise.all(
|
||||
promises.map(p => p
|
||||
.then(value => ({
|
||||
status: "fulfilled",
|
||||
value
|
||||
}))
|
||||
.catch(reason => ({
|
||||
status: "rejected",
|
||||
reason
|
||||
}))
|
||||
)
|
||||
));
|
||||
}
|
||||
Executable
+173
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* @category Core
|
||||
* @subcategory Application
|
||||
*/
|
||||
class Controller {
|
||||
|
||||
static _template = null;
|
||||
static _contents = [];
|
||||
static _currentContent = null;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
destructor() {}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {*} name
|
||||
* @param {*} args
|
||||
* @param {*} data
|
||||
* @returns {View}
|
||||
*/
|
||||
loadView(name, args, data) {
|
||||
args = args || {};
|
||||
args.name = name+'.html';
|
||||
args.className = name.split('/').pop();
|
||||
args.onContentLoaded = args.onContentLoaded || this.appendHTML;
|
||||
args.path = app.config.router.viewsPath;
|
||||
|
||||
let request = app.Assets.loadHtml(args);
|
||||
|
||||
return request.then(args.onContentLoaded.bind(this, args, data));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} args
|
||||
* @param {*} data
|
||||
* @param {*} html
|
||||
* @returns {View}
|
||||
*/
|
||||
appendHTML(args,data, html) {
|
||||
|
||||
if(!args.target) {
|
||||
args.target = document.body;
|
||||
} else {
|
||||
if(typeof args.target == 'string') {
|
||||
let selector = null;
|
||||
let content = document.body;
|
||||
|
||||
let crumbs = args.target.split('@')
|
||||
|
||||
if(crumbs.length > 1) {
|
||||
let targetId = crumbs.pop()
|
||||
content = Controller._contents.find(o => o.view._sparcId == targetId).view.el;
|
||||
}
|
||||
|
||||
selector = crumbs.join('@');
|
||||
args.target = content.querySelector(selector);
|
||||
}
|
||||
}
|
||||
|
||||
let container = document.createElement('div');
|
||||
|
||||
container.innerHTML = Controller.processTemplate(args.name, html, data);
|
||||
|
||||
let parent = args.target;
|
||||
|
||||
parent.innerHTML = '';
|
||||
|
||||
while(container.children.length > 0) {
|
||||
parent.appendChild(container.children[0]);
|
||||
}
|
||||
|
||||
let view = new app.LoadedClasses[args.className]();
|
||||
view._className = args.name.replace('.html', '');
|
||||
|
||||
Controller._currentContent = view;
|
||||
Controller._contents.push({view: view, dom: parent, type: 'nested'});
|
||||
view.el = parent;
|
||||
view.el.setAttribute('sparc-id', view._sparcId);
|
||||
view._controller = this;
|
||||
|
||||
//view.DOMContentLoaded(data);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} content
|
||||
*/
|
||||
unloadView(content) {
|
||||
app.events.clear(content.view._sparcId);
|
||||
Controller._contents = Controller._contents.filter(o => o.view._sparcId != content.view._sparcId);
|
||||
content.view.DOMContentRemoved();
|
||||
content.view.el.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} name
|
||||
* @param {*} args
|
||||
*/
|
||||
loadModel(name, args) { }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} id
|
||||
* @returns {View}
|
||||
*/
|
||||
static getContentById(id) { return Controller._contents.find(o => o.view._sparcId == id); }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} name
|
||||
* @param {*} html
|
||||
* @param {*} templateData
|
||||
* @returns {string}
|
||||
*/
|
||||
static processTemplate(name, html, templateData) {
|
||||
|
||||
// Define a function inside of which templateData keys will become local variables
|
||||
// AND where templateData values will become thos variable values ;-)
|
||||
// Drawback is that complex epression
|
||||
function evalaluateTemplate(tpl, params) {
|
||||
var interpretor = Function(...Object.keys(params), 'return('+tpl+');' );
|
||||
return interpretor(...Object.values(params));
|
||||
}
|
||||
|
||||
// Define a function inside of which a string of a variable name becomes that variable
|
||||
// and where we check tht the corresponding (evaluable) expression in templateData is defined
|
||||
function checkExpression(varname, params) {
|
||||
var code = ' try{var t=typeof('+varname+');} catch(e){ t="badexpr"; } return(t)';
|
||||
var interpretor = Function(...Object.keys(params), code );
|
||||
return interpretor(...Object.values(params));
|
||||
}
|
||||
|
||||
// Preflight check to avoid template crashes on undefined expressions
|
||||
const rgxp = /\$\{(.*?)\}/gm;
|
||||
let m = null;
|
||||
|
||||
templateData = templateData || {};
|
||||
|
||||
while ((m = rgxp.exec(html)) !== null) {
|
||||
let safeType = checkExpression(m[1],templateData);
|
||||
|
||||
if(safeType == 'undefined') {
|
||||
// variable undefined in templateData: add an empty one
|
||||
console.warn(`In template ${name}, ${m[1]} is not defined !`);
|
||||
templateData[m[1]]='';
|
||||
} else if(safeType == 'badexpr') {
|
||||
// unevaluable expression : remove it from template
|
||||
console.warn(`In template ${name}, Bad expression: ${m[1]}`);
|
||||
var escsearch = m[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
var replrexp = new RegExp('\\$\\{'+escsearch+'\\}','gm')
|
||||
html = html.replace(replrexp,'');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now that all remaining expressions are OK, evaluate the template via backtits !
|
||||
return(evalaluateTemplate('`' + html + '`', templateData));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
app.registerClass("Controller", Controller);
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @category Core
|
||||
* @subcategory Application
|
||||
* @extends Controller
|
||||
*/
|
||||
class MasterController extends Controller {
|
||||
|
||||
content = null;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
if(app.MessageBus) this.onBusEnabled()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on init, if bus enabled (probably not yet connected)
|
||||
*/
|
||||
onBusEnabled() { app.MessageBus.whenConnected(this.onBusConnected.bind(this)) }
|
||||
|
||||
/** Called on bus connection
|
||||
* Might be called again upon drop+reconnect
|
||||
* Good place to put general subscriptions, to ensure
|
||||
* you'll resubscribe after drop+reconnect
|
||||
*/
|
||||
onBusConnected(){ }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} name
|
||||
* @returns {View}
|
||||
*/
|
||||
useTemplate(name) {
|
||||
let className = name.split('/').pop(0);
|
||||
let args = {};
|
||||
|
||||
if(name) {
|
||||
if(!Controller._template || (Controller._template.name != className)) {
|
||||
args.onContentLoaded = this.onTemplateLoaded;
|
||||
return(this.loadView(name, args));
|
||||
}
|
||||
}
|
||||
return(new Promise((resolve) => { resolve(null); }));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} args
|
||||
* @param {*} data
|
||||
* @param {*} html
|
||||
*/
|
||||
onTemplateLoaded(args,data, html) {
|
||||
let container = document.createElement('div');
|
||||
container.innerHTML = html;
|
||||
|
||||
this.el = document.body;
|
||||
|
||||
this.el.innerHTML = '';
|
||||
|
||||
while(container.children.length > 0) {
|
||||
this.el.appendChild(container.children[0]);
|
||||
}
|
||||
|
||||
let view = new app.LoadedClasses[args.className]();
|
||||
|
||||
Controller._template = {name: args.className, view: view, dom: this.el};
|
||||
|
||||
view.el = this.el;
|
||||
|
||||
view.DOMContentLoaded();
|
||||
this.content = Controller._template.view;
|
||||
this.ControllerReady();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
ControllerReady() {}
|
||||
}
|
||||
|
||||
app.registerClass('MasterController', MasterController);
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Base model class
|
||||
*
|
||||
* @author Nicolas Stein
|
||||
* @author Michael Fallise
|
||||
* @version 1.2
|
||||
* @category Core
|
||||
* @subcategory Application
|
||||
*/
|
||||
class Model {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} response
|
||||
* @returns {object}
|
||||
*/
|
||||
_processResponse(response, resolve, reject){
|
||||
if(response.ok){ // All ok
|
||||
if( (response.headers.get('Content-Type')) && (response.headers.get('Content-Type').toLocaleLowerCase().indexOf('application/json')>-1) ) {
|
||||
resolve(response.json());
|
||||
} else {
|
||||
resolve(
|
||||
response.text().then(rawResponse => {
|
||||
if(rawResponse != '') {
|
||||
console.warn('Server did not declare response as Json...trying to parse it anyway', rawResponse)
|
||||
return(JSON.parse(rawResponse));
|
||||
} else if(response.status==204) return(true); // Normal to have no content, so make sure other layers see it as success
|
||||
else return('');
|
||||
})
|
||||
)
|
||||
}
|
||||
} else { // server error
|
||||
// server error with json error details
|
||||
if( (response.headers.get('Content-Type')) && (response.headers.get('Content-Type').toLocaleLowerCase().indexOf('application/json')>-1) ){
|
||||
response.json().then(
|
||||
cleanJson => { // json error details is clean
|
||||
let error = {
|
||||
'code': response.status,
|
||||
'displayMessage': (cleanJson.error.displayMessage || ' !!No debugMessage from server!!'),
|
||||
'debugMessage': response.statusText+'\n'+(cleanJson.error.debugMessage || ' !!No debugMessage from server!!'),
|
||||
}
|
||||
this.onRequestError(error);
|
||||
reject(error);
|
||||
},
|
||||
text => {
|
||||
let error = { // json error details is garbage
|
||||
'code': response.status,
|
||||
'displayMessage': 'Could not request to server!?',
|
||||
'debugMessage': 'Additionally, bad Json returned :', text,
|
||||
}
|
||||
this.onRequestError(error);
|
||||
reject(error);
|
||||
}
|
||||
)
|
||||
} else { // server error with text return
|
||||
response.text().then(
|
||||
text => {
|
||||
let error = {
|
||||
'code': response.status,
|
||||
'displayMessage': 'Could not request to server!?',
|
||||
'debugMessage': `Server responded: ${text}`,
|
||||
}
|
||||
this.onRequestError(error);
|
||||
reject(error);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic API service call
|
||||
* @param {string} uri
|
||||
* @param {string} method
|
||||
* @param {object} payload
|
||||
* @returns {object}
|
||||
*/
|
||||
request(uri, method, payload) {
|
||||
// Fetch chooses its own resolve/reject (see https://developer.mozilla.org/en-US/docs/Web/API/fetch)
|
||||
// Errors like 404 are not rejected. and we can't change the -already done- resolve/reject choice.
|
||||
// therefore we return our own promise.
|
||||
if((!method) || (!uri)) {
|
||||
return(
|
||||
new Promise((resolve, reject) => reject(
|
||||
{ sucess :false,
|
||||
payload: null,
|
||||
error: { displayError: 'Server Error (no endpoint)',
|
||||
debugError: 'Missing endpoint' },
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
new Promise((resolve, reject) => {
|
||||
let body = null;
|
||||
if(method.toLowerCase()!='get') body = payload ? JSON.stringify(payload): null;
|
||||
else if(payload) uri += '?'+Object.entries(payload).map(kv => kv.map(encodeURIComponent).join("=")).join("&");
|
||||
fetch(uri, {
|
||||
method: method,
|
||||
headers: { 'Accept': 'application/json', 'Content-Type':'application/json'},
|
||||
body: body,
|
||||
credentials: 'include', //By nike: mandatory for MT to get session !!!
|
||||
})
|
||||
.then( response => {
|
||||
this._processResponse(response, resolve, reject);
|
||||
})
|
||||
.catch(debugMessage => { // Other errors like metwork, protocol, CORs errors, or 200 but bad JSON
|
||||
let error = {
|
||||
'displayMessage': 'Could not request to server!?',
|
||||
'debugMessage': debugMessage,
|
||||
'code': '4XX' // non http but >299
|
||||
}
|
||||
this.onRequestError(error);
|
||||
reject(error);
|
||||
})
|
||||
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request error handler
|
||||
* @param {object} error
|
||||
*/
|
||||
onRequestError(error) { console.error('Ajax request error:', error); }
|
||||
}
|
||||
|
||||
app.registerClass('Model', Model)
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
'use strict'
|
||||
if(typeof(app)=='undefined') var app = {};
|
||||
/**
|
||||
__ __
|
||||
( )( ) ___ ____ ____
|
||||
)( )( / __)( ___)( _ \
|
||||
)(__)( \__ \ )__) ) /
|
||||
(______)(___/(____)(_)\_)
|
||||
By Nike
|
||||
|
||||
This file is part of Widgets by Nike from Nicsys (info@nicsys.eu).
|
||||
Widgets is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
either version 3 of the License, or (at your option) any later version.
|
||||
Widgets is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
Get your copy of the GNU General Public License at <https://www.gnu.org/licenses/>.
|
||||
* @category Core
|
||||
* @subcategory Application
|
||||
*/
|
||||
class User {
|
||||
authenticationDone = false;
|
||||
isAuthenticated = false;
|
||||
identity = {};
|
||||
roles = [];
|
||||
|
||||
constructor(){ }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} callBack
|
||||
*/
|
||||
checkAuthenticated(callBack){
|
||||
this.authenticationDone = true
|
||||
this.isAuthenticated = true
|
||||
this.userInfo = {
|
||||
'identity':{},
|
||||
'roles': [],
|
||||
};
|
||||
callBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* do nothing
|
||||
*/
|
||||
gotoLogin() { }
|
||||
|
||||
/**
|
||||
* do nothing
|
||||
*/
|
||||
getMessageBusUserInfo() { }
|
||||
}
|
||||
app.LoadedClasses.User = User
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @category Core
|
||||
* @subcategory Application
|
||||
*/
|
||||
class View {
|
||||
|
||||
_sparcId = null;
|
||||
_className = null;
|
||||
_controller = null;
|
||||
|
||||
title = "";
|
||||
description = "";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} parms
|
||||
*/
|
||||
constructor(parms) {
|
||||
parms = parms || {};
|
||||
this._sparcId = crypto.randomUUID();
|
||||
this.title = parms.title || '';
|
||||
this.description = parms.description || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* base method called when content is appended to the document.
|
||||
*/
|
||||
DOMContentLoaded() {}
|
||||
|
||||
/**
|
||||
* base method called when content is removed from the document.
|
||||
*/
|
||||
DOMContentFocused() {}
|
||||
|
||||
/**
|
||||
* base method called when content is removed from the document.
|
||||
*/
|
||||
DOMContentBlured() {}
|
||||
|
||||
/**
|
||||
* base method called when content is removed from the document.
|
||||
*/
|
||||
DOMContentRemoved() {}
|
||||
/**
|
||||
* acts as a shortcut for vanilla querySelector Element method.
|
||||
* @param {*} selector
|
||||
* @returns {Element|null}
|
||||
*/
|
||||
find(selector) { return this.el ? this.el.querySelector(selector): null; }
|
||||
|
||||
/**
|
||||
* acts as a shortcut for vanilla querySelectorAll Element method.
|
||||
* @param {*} selector
|
||||
* @returns {Array<Element>}
|
||||
*/
|
||||
findAll(selector) { return this.el ? this.el.querySelectorAll(selector): null; }
|
||||
|
||||
/**
|
||||
* wrapper for vanilla addEventListener Element method.
|
||||
* @param {*} type
|
||||
* @param {*} callback
|
||||
*/
|
||||
addEvent(type, callback) { app.addEvent(type, callback, this._sparcId); }
|
||||
|
||||
/**
|
||||
* AddEvent to all found selector.
|
||||
* @param {*} selector
|
||||
* @param {*} type
|
||||
* @param {*} callback
|
||||
*/
|
||||
addEventTo(selector, type, callback) {
|
||||
for(let el of this.findAll(selector)) {
|
||||
// Cannot use the wrapper because not attached to el, but to window
|
||||
// app.addEvent(type, callback, this._sparcId);
|
||||
//TODO: see with Mike if addEvent could include target element .
|
||||
el.addEventListener(type, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action to all found selector.
|
||||
* @param {*} selector
|
||||
* @param {*} type
|
||||
* @todo what is this used for ? deprecate ?
|
||||
*/
|
||||
addActions(selector, type) {
|
||||
this.addEventTo(selector+'[action]', type, e => {
|
||||
let ax = this['action_'+e.target.getAttribute('action')];
|
||||
if(typeof(ax)== 'function') ax.bind(this)(e);
|
||||
else console.warn('Bad action: '+e.target.getAttribute('action'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* aggregates the content inner key to the targeted selector, avoiding targeting other content.
|
||||
* @param {string} selector
|
||||
* @returns {string}
|
||||
*/
|
||||
scope(selector) { return `${selector}@${this._sparcId}`; }
|
||||
|
||||
/**
|
||||
* Wrapper for Controller.loadView
|
||||
* @param {*} name
|
||||
* @param {*} args
|
||||
* @async
|
||||
* @returns {View}
|
||||
*/
|
||||
loadView(name, args) { return this._controller.loadView(name, args); }
|
||||
|
||||
}
|
||||
|
||||
app.registerClass('View', View)
|
||||
Executable
+291
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
|
||||
__ ___ ___ ____ ____ ___
|
||||
/__\ / __)/ __)( ___)(_ _)/ __)
|
||||
/(__)\ \__ \\__ \ )__) )( \__ \
|
||||
(__)(__)(___/(___/(____) (__) (___/ for SPARC
|
||||
By Mike & Nike
|
||||
|
||||
This file is part of Sparc by Mike & Nike.
|
||||
Sparc is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License,
|
||||
as published by the Free Software Foundation,
|
||||
either version 3 of the License, or (at your option) any later version.
|
||||
Sparc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
Get your copy of the GNU General Public License at <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
* @category Core
|
||||
* @subcategory Libraries
|
||||
* @hideconstructor
|
||||
*/
|
||||
|
||||
class Assets {
|
||||
|
||||
static defaults = {};
|
||||
/**
|
||||
* @type {object}
|
||||
* @property {Array<object>} images
|
||||
* @property {Array<object>} css
|
||||
* @property {Array<object>} html
|
||||
* @property {Array<object>} json
|
||||
* @property {Array<object>} fonts
|
||||
* @property {Array<object>} sfx
|
||||
*/
|
||||
static Store = { 'images':{}, 'css':{}, 'html':{}, 'json':{}, 'fonts':{}, 'sfx':{} };
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {*} args
|
||||
* @param {string} args.path
|
||||
* @param {string} args.name
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.refresh]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static getImage(args){
|
||||
if( (!args.hasOwnProperty('path')) || (args.path=='') ) var fpath = this.defaults.basePath+'images/'+args.name;
|
||||
else var fpath = (args.path+'/'+args.name).replace(/\/+/g,'/').replace(/http(s)?:\//g, 'http$1://');
|
||||
let aid = args.id || fpath;
|
||||
if(this.Store.images.hasOwnProperty(aid) && (!args.refresh)) {
|
||||
return(new Promise((resolve) => {
|
||||
resolve(this.Store.images[aid]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let node = document.createElement('img');
|
||||
return(new Promise( (resolve, fail) => {
|
||||
node.onload = (e) => {
|
||||
this.Store.images[aid] = node;
|
||||
resolve(node);
|
||||
}
|
||||
node.onerror= (e) => {
|
||||
console.warn('Could not load IMAGE asset ',aid, fpath);
|
||||
}
|
||||
node.src = fpath+'?'+crypto.randomUUID();
|
||||
// for img nodes, setting the src is sufficient to start loading, then trigger the onload
|
||||
})
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @async
|
||||
* @param {*} args
|
||||
* @param {string} args.path
|
||||
* @param {string} args.name
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.refresh]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static loadJson(args){
|
||||
if( (!args.hasOwnProperty('path')) || (args.path=='') ) var fpath = this.defaults.basePath+'json/'+args.name;
|
||||
else var fpath = (args.path+args.name).replace(/\/+/g,'/').replace(/http(s)?:\//g, 'http$1://');
|
||||
let aid = args.id || fpath;
|
||||
if( (this.Store.json.hasOwnProperty(aid)) && (!args.refresh) ){
|
||||
return(new Promise((resolve) => {
|
||||
resolve(this.Store.json[aid]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return( fetch(fpath+'?'+crypto.randomUUID())
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.Store.json[aid] = data;
|
||||
return(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('Could not load json asset:', aid, fpath, error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @async
|
||||
* @param {*} args
|
||||
* @param {string} args.path
|
||||
* @param {string} args.name
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.refresh]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static loadCss(args){
|
||||
if( (!args.hasOwnProperty('path')) || (args.path=='') ) var fpath = this.defaults.basePath+'styles/'+args.name;
|
||||
else var fpath = (args.path+args.name).replace(/\/+/g,'/').replace(/http(s)?:\//g, 'http$1://');
|
||||
let aid = args.id || fpath;
|
||||
|
||||
let _notInIndex = (aid) => {
|
||||
if( (!app.config.squeeze) || (!app.config.squeeze.packages) ) return(true)
|
||||
for(let dragee of app.config.squeeze.packages) {
|
||||
let ext = dragee.target.toLowerCase().substring(dragee.target.lastIndexOf('.'))
|
||||
if((ext!='.css') || (!dragee.setIndexPage)) continue
|
||||
for(let file of dragee.sources) {
|
||||
if(file == aid) return(false)
|
||||
}
|
||||
}
|
||||
return(true)
|
||||
}
|
||||
|
||||
if( this.Store.css.hasOwnProperty(aid) && (!args.refresh) ) {
|
||||
return(new Promise((resolve) => {
|
||||
resolve(this.Store.css[aid]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let node;
|
||||
if( (this.Store.css.hasOwnProperty(aid)) && (args.refresh) ) node = this.Store.css[aid];
|
||||
else {
|
||||
node = document.createElement('link');
|
||||
node.setAttribute('rel','stylesheet');
|
||||
node.setAttribute('type','text/css');
|
||||
}
|
||||
return( new Promise( (resolve, fail) => {
|
||||
node.onload = (e) => {
|
||||
this.Store.css[aid] = node;
|
||||
resolve(node);
|
||||
}
|
||||
node.onerror= (e) => {
|
||||
console.warn('Could not load CSS asset ', aid, fpath);
|
||||
fail(node);
|
||||
}
|
||||
node.setAttribute('href',fpath+'?'+crypto.randomUUID());
|
||||
// for link nodes, setting the href is NOT sufficient to start loading
|
||||
// , then trigger the onload. Must add it to the dom !
|
||||
document.head.appendChild(node)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an audio file.
|
||||
* @async
|
||||
* @param {*} args
|
||||
* @param {string} args.path
|
||||
* @param {string} args.name
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.refresh]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static loadSound(args){
|
||||
if( (!args.hasOwnProperty('path')) || (args.path=='') ) var fpath = this.defaults.basePath+'sfx/'+args.name;
|
||||
else var fpath = (args.path+args.name).replace(/\/+/g,'/').replace(/http(s)?:\//g, 'http$1://');
|
||||
let aid = args.id || fpath;
|
||||
if( (this.Store.sfx.hasOwnProperty(aid)) && (!args.refresh) ) {
|
||||
return(new Promise((resolve) => {
|
||||
resolve(this.Store.sfx[aid]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return(new Promise((resolve, fail) => {
|
||||
this.Store.sfx[aid] = new Audio();
|
||||
this.Store.sfx[aid].setAttribute('aid', aid);
|
||||
this.Store.sfx[aid].addEventListener('canplaythrough', (e) => resolve(e.target), { 'once':true });
|
||||
this.Store.sfx[aid].addEventListener('error', (e) => fail(e.target), { 'once':true });
|
||||
this.Store.sfx[aid].src = fpath+'?'+crypto.randomUUID();
|
||||
return(this.Store.sfx[aid]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a loaded audio, loads it first if needed.
|
||||
* @async
|
||||
* @param {*} args
|
||||
* @param {string} args.path
|
||||
* @param {string} args.name
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.refresh]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static playSound(args){
|
||||
// Nike: Promise when fulfilled when sfx loaded.
|
||||
// If we want when sfx finished to play then
|
||||
// stick a 'ended' event listener as resolver in a new promise
|
||||
return(
|
||||
this.loadSound(args).then( (snd) => { snd.play(); })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @async
|
||||
* @param {*} args
|
||||
* @param {string} args.path
|
||||
* @param {string} args.name
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.refresh]
|
||||
* @returns {Promise}
|
||||
* @todo check issue with undeclared aid
|
||||
*/
|
||||
static loadFont(args){
|
||||
// something fishy here => aid not declared
|
||||
let acronym = aid || args.name.substr(0,args.name.lastIndexOf('.'));
|
||||
|
||||
if( (!args.hasOwnProperty('path')) || (args.path=='') ) var fpath = this.defaults.basePath+'fonts/'+args.name;
|
||||
else var fpath = (args.path+args.name).replace(/\/+/g,'/').replace(/http(s)?:\//g, 'http$1://');
|
||||
let aid = args.id || fpath;
|
||||
if( (this.Store.fonts.hasOwnProperty(aid)) && (!args.refresh) ) {
|
||||
return(new Promise((resolve) => {
|
||||
resolve([loaded_face, acronym]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let ff = new FontFace(acronym,'url('+fpath+'?'+crypto.randomUUID()+')');
|
||||
return(ff.load()
|
||||
.then(loaded_face => {
|
||||
document.fonts.add(loaded_face);
|
||||
this.Store.fonts[aid] = loaded_face;
|
||||
return([loaded_face, acronym]);
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('Could not load FONT asset:' + fpath);
|
||||
[null, acronym]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an HTML file
|
||||
* @async
|
||||
* @param {*} args
|
||||
* @param {string} args.path
|
||||
* @param {string} args.name
|
||||
* @param {string} [args.id]
|
||||
* @param {boolean} [args.refresh]
|
||||
* @returns {string}
|
||||
*/
|
||||
static loadHtml(args) {
|
||||
if( (!args.hasOwnProperty('path')) || (args.path=='') ) var fpath = this.defaults.basePath+'html/'+args.name;
|
||||
else var fpath = (args.path+args.name).replace(/\/+/g,'/').replace(/http(s)?:\//g, 'http$1://');
|
||||
let aid = args.id || fpath;
|
||||
if( (this.Store.html.hasOwnProperty(aid)) && (!args.refresh) ) {
|
||||
let html = this.Store.html[aid];
|
||||
return(new Promise((resolve) => {
|
||||
resolve(html);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return(fetch(fpath + '?' + crypto.randomUUID())
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
this.Store.html[aid] = html;
|
||||
return html;
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('Could not load HTML asset:', aid, fpath);
|
||||
return(null);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app.LoadedClasses.Assets = Assets;
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
*
|
||||
* @category Core
|
||||
* @subcategory Libraries
|
||||
*/
|
||||
class Events {
|
||||
|
||||
// collection of registered app events
|
||||
static _registered = [];
|
||||
|
||||
constructor() {
|
||||
this.channel = document.body;
|
||||
}
|
||||
/**
|
||||
* Candidate for deprecation: registering callbacks sounds like shit load of issues
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {*} callback
|
||||
* @param {string} scope
|
||||
*/
|
||||
addEvent(type, callback, scope) {
|
||||
let available = Events._registered.find(o => o.type == type && o.scope == scope);
|
||||
|
||||
if(!available) {
|
||||
switch(type) {
|
||||
case 'resize':
|
||||
window.addEventListener(type, callback);
|
||||
break;
|
||||
default:
|
||||
this.channel.addEventListener(type, callback);
|
||||
}
|
||||
|
||||
Events._registered.push({scope: scope, type: type, callback: callback})
|
||||
}
|
||||
}
|
||||
|
||||
removeEvent(type, callback, scope) {
|
||||
let index = Events._registered.findIndex(o => o.type == type && o.scope == scope);
|
||||
|
||||
if(index != -1) {
|
||||
switch(type) {
|
||||
case 'resize':
|
||||
window.removeEventListener(type, callback);
|
||||
break;
|
||||
default:
|
||||
this.channel.removeEventListener(type, callback);
|
||||
}
|
||||
|
||||
Events._registered.splice(index,1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {*} data
|
||||
*/
|
||||
trigger(type, data) {
|
||||
|
||||
let event = new CustomEvent(type, {detail: data});
|
||||
|
||||
switch(type) {
|
||||
case 'resize':
|
||||
window.dispatchEvent(event);
|
||||
break;
|
||||
default:
|
||||
this.channel.dispatchEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} scope
|
||||
*/
|
||||
clear(scope) {
|
||||
let i = 0;
|
||||
while(i < Events._registered.length) {
|
||||
let event = Events._registered[i];
|
||||
|
||||
if(event.scope == scope) {
|
||||
switch(event.type) {
|
||||
case 'resize':
|
||||
window.removeEventListener(event.type, event.callback);
|
||||
break;
|
||||
default:
|
||||
document.body.removeEventListener(event.type, event.callback);
|
||||
}
|
||||
Events._registered.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerClass('Events', Events);
|
||||
Executable
+145
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
|
||||
__
|
||||
( ) _____ ___ ___ ____ ____
|
||||
)( ( _ )/ __) / __)( ___)( _ \
|
||||
)(__ )(_)(( (_-.( (_-. )__) ) /
|
||||
(____)(_____)\___/ \___/(____)(_)\_)
|
||||
By Mike & Nike
|
||||
|
||||
This file is part of Sparc by Mike & Nike.
|
||||
Widgets is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
either version 3 of the License, or (at your option) any later version.
|
||||
Widgets is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
Get your copy of the GNU General Public License at <https://www.gnu.org/licenses/>.
|
||||
* @category Core
|
||||
* @subcategory Libraries
|
||||
* @class
|
||||
* @todo extract from app (this is just a hook for window.console)
|
||||
* @param {object} nativeConsole a reference to the native browser console (Typically window.console )
|
||||
* @param {object} config
|
||||
*/
|
||||
app.logger = function(oldCons, config){
|
||||
const appendErr = (data) => {
|
||||
data['errId'] = crypto.randomUUID()
|
||||
app.latestErrors.push(data)
|
||||
let maxentries = config.latestErrsMax || 10;
|
||||
while(app.latestErrors.length > maxentries) app.latestErrors.shift()
|
||||
}
|
||||
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return;
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
const formater = function(err, args, color){
|
||||
var splitter = function(stkLine, mode){
|
||||
if(mode=='chrome'){
|
||||
var buf = stkLine.substr(0,stkLine.indexOf(' ('));
|
||||
var funcName = buf.substr(buf.lastIndexOf('.')+1);
|
||||
var buf = stkLine.substr(stkLine.lastIndexOf('/')+1);
|
||||
var scriptname = buf.substr(0,buf.indexOf('.js')+3);
|
||||
var lincol = buf.substr(buf.indexOf(':')+1).split(':');
|
||||
} else {
|
||||
var funcName = stkLine.substr(0,stkLine.indexOf('@'));
|
||||
if(funcName.indexOf('/')>-1) funcName = funcName.substr(0,funcName.indexOf('/'));
|
||||
var buf = stkLine.substr(stkLine.lastIndexOf('/')+1);
|
||||
var scriptname = buf.substr(0,buf.indexOf('.js')+3);
|
||||
var lincol = buf.substr(buf.indexOf(':')+1).split(':');
|
||||
}
|
||||
return([funcName, scriptname, lincol]);
|
||||
}
|
||||
var lines = err.stack.split('\n'); var nice_stack=[];
|
||||
var funcName, scriptname, lincol;
|
||||
if(lines[0].toLowerCase()=='error'){ // Chrome & Edge style
|
||||
[funcName, scriptname, lincol] = splitter(lines[2], 'chrome');
|
||||
lines.shift();lines.shift();
|
||||
for(var line of lines) nice_stack.push(splitter(line, 'chrome'));
|
||||
} else { // Firefox style
|
||||
[funcName, scriptname, lincol] = splitter(lines[1], 'ff');
|
||||
lines.shift();
|
||||
for(var line of lines) nice_stack.push(splitter(line, 'ff'));
|
||||
}
|
||||
var msg = args[0];
|
||||
args[0] = `${args[0]} %c${funcName} @ ${scriptname} : ${lincol}`;
|
||||
args.splice(1,0,'background-color:'+color+';color:#FFF;font-weight:bold;float:right;padding:2px 5px 2px 5px;border-radius:4px;');
|
||||
return([args, msg, nice_stack]);
|
||||
}
|
||||
return {
|
||||
nativeConsoleAPI:'', //noooo, the console is NOT changed... or is it ;-)
|
||||
trace: oldCons.trace,
|
||||
log: function(...args){
|
||||
if(typeof(args[0])!='string') args.unshift('');
|
||||
var nice_stack, msg;
|
||||
var err = new Error();
|
||||
[args, msg, nice_stack] = formater(err, args, '#070');
|
||||
oldCons.log(...args);
|
||||
},
|
||||
info: function (...args) {
|
||||
if(typeof(args[0])!='string') args.unshift('');
|
||||
var nice_stack, msg;
|
||||
var err = new Error();
|
||||
[args, msg, nice_stack] = formater(err, args, '#070');
|
||||
oldCons.info(...args);
|
||||
},
|
||||
warn: function (...args) {
|
||||
if(typeof(args[0])!='string') args.unshift('');
|
||||
var nice_stack, msg;
|
||||
var err = new Error();
|
||||
var msgData = (args.length>1) ? args.slice(1) : '';
|
||||
[args, msg, nice_stack] = formater(err, args, '#C90');
|
||||
oldCons.warn(...args);
|
||||
if(config.levels.indexOf('warn')<0) return;
|
||||
let data = {'level':'WARNING',
|
||||
'message': msg,
|
||||
'messageData' : msgData,
|
||||
'user': app.User,
|
||||
'timestamp':(new Date).toISOString().replace(/[A-Z]/g,' ').trim() ,
|
||||
'url': document.location.toString(),
|
||||
'stacktrace' : nice_stack,
|
||||
};
|
||||
if(config.postUrl.startsWith('https://')){
|
||||
fetch(config.postUrl, {
|
||||
'method': 'POST',
|
||||
'body' : JSON.stringify(data, getCircularReplacer()),
|
||||
'headers': {'Content-type': 'application/json; charset=UTF-8'}
|
||||
})
|
||||
}
|
||||
appendErr(data);
|
||||
},
|
||||
error: function (...args) {
|
||||
if(typeof(args[0])!='string') args.unshift('');
|
||||
var nice_stack, msg;
|
||||
var err = new Error();
|
||||
var msgData = (args.length>1) ? args.slice(1) : '';
|
||||
[args, msg, nice_stack] = formater(err, args, '#900');
|
||||
oldCons.error(...args);
|
||||
if(config.levels.indexOf('err')<0) return;
|
||||
let data = {'level':'ERROR',
|
||||
'message': msg,
|
||||
'messageData' : msgData,
|
||||
'user': app.User,
|
||||
'timestamp':(new Date).toISOString().replace(/[A-Z]/g,' ').trim() ,
|
||||
'url': document.location.toString(),
|
||||
'stacktrace' : nice_stack,
|
||||
};
|
||||
if(config.postUrl.startsWith('https://')){
|
||||
fetch(config.postUrl, {
|
||||
'method': 'POST',
|
||||
'body' : JSON.stringify(data, getCircularReplacer()),
|
||||
'headers': {'Content-type': 'application/json; charset=UTF-8'}
|
||||
})
|
||||
}
|
||||
appendErr(data);
|
||||
}
|
||||
};
|
||||
};
|
||||
Executable
+444
@@ -0,0 +1,444 @@
|
||||
'use strict'
|
||||
/**
|
||||
* PROTOCOL
|
||||
|
||||
|
||||
Application-level payloads are always JSON and always either an action, or an event :
|
||||
|
||||
1. ACTIONS : are made for request-reply.
|
||||
They are aimed at the dialogue between the FE (mainly messageBus core modules) and WSSGateway.
|
||||
These messages are identified by the fact there is an "action" key, top level.
|
||||
|
||||
Example : The FE asks WSSGateway to subscribe to Redis chans :
|
||||
Request:
|
||||
{ "action" : "SUB", // Must be a valid wssGateway action.
|
||||
"payload": ["chan1", "chan2"], // Any type required by the action
|
||||
"reqid": "987654321-abcdef-123456"
|
||||
}
|
||||
|
||||
Reply:
|
||||
{ "action" : "SUB",
|
||||
"payload": ["chan1", "chan9"], // probably you were already subscribed to chan9,
|
||||
"reqid": "987654321-abcdef-123456" // don't have to right to chan2, but succeeded subscribing to chan1
|
||||
}
|
||||
|
||||
Newton principle applied to WSSG:
|
||||
When there is an action in one direction (request),
|
||||
there is the same action in the opposite direction (reply).
|
||||
|
||||
When doing a request, the FE can optionally include a "reqid", with a uuid.
|
||||
It then has the guarnatee that the corresponding reply will contain the same reqid.
|
||||
As you can receive a reply on a particular action in any number, at any time,
|
||||
this allows the FE to match one specific action request with its specific reply.
|
||||
This, in turn, allows this module to provide action-promise and action-timeouts.
|
||||
|
||||
2. EVENTS : are any other events circulating on the bus, thus on a REDIS channel.
|
||||
They are triggered by another actor on the bus, and have nothing to do with FE-WssGW dialog .
|
||||
These messages are identified by the fact there is an "event" key, top level.
|
||||
So far, this core-module has no use of bus-events, they are considered as applicative-level-use only.
|
||||
Therefore, this module just triggers a corresponding (javascript) event, for any potential listener in the app.
|
||||
|
||||
{ "eventType" : "PropaSubmitted", // Any applicative thing
|
||||
"payload": { // Any type depending on applicative convention for this event
|
||||
"propaNumber": "123456",
|
||||
"propaAcronym": "Tintin"
|
||||
}
|
||||
}
|
||||
|
||||
Will trigger a "MessageBus.PropaSubmitted" javascript event, with
|
||||
"detail":
|
||||
{ msg: {
|
||||
eventType: "PropaSubmitted",
|
||||
payload: { "propaNumber": "123456",
|
||||
"propaAcronym": "Tintin"
|
||||
}
|
||||
},
|
||||
chan: "wssGateway:chan1:subchan2",
|
||||
}
|
||||
|
||||
--------------- Low-level, WEBSOCKET ---------------
|
||||
{ "event":"REDISMSG", // low level
|
||||
"payload":{ // low level
|
||||
"msg":{ // low level
|
||||
"eventType":"PropaSubmitted", // APP LEVEL MESSAGE = Redis payload
|
||||
"payload":{ // APP LEVEL MESSAGE = Redis payload
|
||||
"propaNumber": "123456", // APP LEVEL MESSAGE = Redis payload
|
||||
"propaAcronym": "Tintin" // APP LEVEL MESSAGE = Redis payload
|
||||
}, // APP LEVEL MESSAGE = Redis payload
|
||||
sender: "N007xyz" // APP LEVEL MESSAGE = Redis payload => added by gateways !
|
||||
}, // low level
|
||||
"chan":"wssGateway:chan1:subchan2" // low level = Redis channel
|
||||
},
|
||||
}
|
||||
*
|
||||
* @author Nicolas Stein
|
||||
* @category Core
|
||||
* @subcategory Libraries
|
||||
*/
|
||||
class MessageBus {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} config
|
||||
* @param {*} userInfo
|
||||
*/
|
||||
constructor(config, userInfo){
|
||||
this.config = config
|
||||
if(this.config.debug) console.log('Lauching Websocket worker...');
|
||||
this.config.hostname = (('host' in this.config) && ( this.config.host!='')) ? this.config.host : document.location.hostname
|
||||
this.userInfo = userInfo
|
||||
this.createWorker();
|
||||
this.activeSubscriptions = [];
|
||||
this.promisesRegister = { };
|
||||
this.bus2jsEventsRegister = []; // items: { eventType:'string', RegisteredCb: function, realCb: function }
|
||||
this.whenConnectedQ = [];
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
createWorker() {
|
||||
if(!this.config.pathToWorker.endsWith('.js')) this.config.pathToWorker+='.js';
|
||||
this.MessageBusWorker = new Worker(this.config.pathToWorker+'?'+crypto.randomUUID());
|
||||
this.MessageBusWorker.postMessage({ 'action':'start', 'config': this.config, 'userInfo': this.userInfo });
|
||||
this.MessageBusWorker.onmessage = this.receiveFromWorker.bind(this);
|
||||
if(this.config.debug) console.log('Websocket worker launched.');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} callBack
|
||||
*/
|
||||
whenConnected(callBack){
|
||||
if(typeof(callBack) != 'function') return;
|
||||
if(this.connected) callBack();
|
||||
else this.whenConnectedQ.push(callBack);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} timeout
|
||||
* @returns {Promise}
|
||||
*/
|
||||
whenConnectedP(timeout=0){
|
||||
return(
|
||||
new Promise((resolve,reject) => {
|
||||
this.whenConnected(resolve)
|
||||
if(timeout>0) setTimeout(reject, timeout)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} callBack
|
||||
*/
|
||||
ifConnected(callBack){
|
||||
if(typeof(callBack) != 'function') return;
|
||||
if(this.connected) callBack();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
executewhenConnectedQ() { for(var callBack of this.whenConnectedQ) callBack(); }
|
||||
|
||||
|
||||
/**
|
||||
* Request-reply an action from the WSSGateway
|
||||
* This is a pure websocket exchange between client and WssGW.
|
||||
* This request does not pass through the (Redis) bus.
|
||||
* This method gives (and resolves) a promise, taking care of all lower-level details
|
||||
*/
|
||||
requestWssGwAction(action, payload=null, timeOut=5000){
|
||||
if(!action) return;
|
||||
let request = {'action':action, 'payload':payload};
|
||||
request.reqid = crypto.randomUUID();
|
||||
return(new Promise((resolve, fail) => {
|
||||
let timeOutID = setTimeout(() => {
|
||||
fail(`Timeout (>${timeOut}ms) for action ${action}`);
|
||||
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
|
||||
}, timeOut);
|
||||
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID];
|
||||
this.MessageBusWorker.postMessage(request);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request-reply an action towards an agent on the bus (normally infra, like HttpGw)
|
||||
* This request will pass through the (Redis) bus.
|
||||
* The reply will come on my own user notification channel.
|
||||
* This method gives (and resolves) a promise, taking care of all lower-level details
|
||||
*/
|
||||
requestBusAction(chan, action, payload=null, timeOut=5000){
|
||||
if(!action) return;
|
||||
let request = {'action':action, 'payload':payload};
|
||||
request.reqid = crypto.randomUUID();
|
||||
return(new Promise((resolve, fail) => {
|
||||
let timeOutID = setTimeout(() => {
|
||||
fail(`Timeout (>${timeOut}ms) for action ${action}`);
|
||||
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
|
||||
}, timeOut);
|
||||
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID];
|
||||
if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan
|
||||
this.send(chan, JSON.stringify(request))
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request-reply an action towards Midas
|
||||
* This request will pass through the (Redis) bus.
|
||||
* The reply will come on my own user notification channel.
|
||||
* This method gives (and resolves) a promise, taking care of all lower-level details
|
||||
*/
|
||||
requestMidasAction(chan, action, data=null, timeOut=5000){
|
||||
if(!action) return;
|
||||
let request = {payload: {'action':action, 'data':data}}
|
||||
request.reqid = crypto.randomUUID();
|
||||
return(new Promise((resolve, fail) => {
|
||||
let timeOutID = setTimeout(() => {
|
||||
fail(`Timeout (>${timeOut}ms) for action ${action}`);
|
||||
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
|
||||
}, timeOut);
|
||||
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID];
|
||||
if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan
|
||||
this.send(chan, JSON.stringify(request))
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} chan
|
||||
* @param {*} eventType
|
||||
* @param {*} eventPayload
|
||||
*/
|
||||
sendEvent(chan, eventType, eventPayload){
|
||||
if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan
|
||||
this.send(
|
||||
chan,
|
||||
JSON.stringify({ eventType: eventType,
|
||||
payload: eventPayload
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} chan
|
||||
* @param {*} msg
|
||||
*/
|
||||
send(chan, msg){
|
||||
// You can publish to an unsubscribed chan, userchans are the best example !
|
||||
// if(this.activeSubscriptions.indexOf(chan)<0) return;
|
||||
var request = {'action':'PUB', 'payload': { 'chan':chan, 'msg': msg}};
|
||||
this.MessageBusWorker.postMessage(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a bus event, filtering on allowed incoming chans.
|
||||
* => Callback takes arguments (chan, eventType, payload)
|
||||
* where chan is the actual chan that carried the event eventType
|
||||
*
|
||||
* Filtering is important because you could have evenType = 'update',
|
||||
* arriving on chans like 'dataChange:proposal' and 'dataChange:organisation' (thus for different actions).
|
||||
* Besides, you don't want to react for example on 'growl' if it's arriving on
|
||||
* some chan publishable by another user and misused by him.
|
||||
*
|
||||
* @param {string} eventType
|
||||
* @param {Array} filterChans Array of allowed chans (string). Globbing with '*' is allowed.
|
||||
* @param {*} callback
|
||||
*/
|
||||
addBusListener(eventType, filterChans, cb, scope=''){
|
||||
let realCb = (e => {
|
||||
let realChan = e.detail.chan
|
||||
if(filterChans.every(filterChan => (!this.chanMatch(realChan, filterChan)))) return
|
||||
cb(realChan, e.detail.payload, e.detail.sender)
|
||||
})
|
||||
let realEventType = 'MessageBus.event.'+eventType
|
||||
app.events.addEvent(realEventType, realCb, 'MessageBus'+scope)
|
||||
this.bus2jsEventsRegister.push({
|
||||
eventType: eventType,
|
||||
cb: cb,
|
||||
realEventType : realEventType,
|
||||
realCb: realCb
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* De-registers bus event(s)
|
||||
* If several events of the same type, same calback, then they are all whiped
|
||||
*/
|
||||
removeBusListener(eventType, cb, scope=''){
|
||||
let toKick = this.bus2jsEventsRegister.filter(
|
||||
item => ((item.eventType==eventType) && (item.cb==cb))
|
||||
)
|
||||
for(let kickItem of toKick){
|
||||
app.events.removeEvent(kickItem.realEventType, kickItem.realCb, 'MessageBus'+scope)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Subscribe to channels
|
||||
*
|
||||
* @param {object} channels
|
||||
* @returns {object}
|
||||
*/
|
||||
subscribe(channels){
|
||||
return(this.requestWssGwAction('SUB', channels))
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from channels
|
||||
*
|
||||
* @param {*} channels
|
||||
* @returns {object}
|
||||
*/
|
||||
unSubscribe(channels) { return(this.requestWssGwAction('UNSUB', channels)) }
|
||||
|
||||
/**
|
||||
* Get current subscriptions list
|
||||
* @returns {object}
|
||||
*/
|
||||
subscriptionsList() { return(this.requestWssGwAction('SUBLST')) }
|
||||
|
||||
/**
|
||||
* Get channel history
|
||||
*
|
||||
* @param {*} channels
|
||||
* @returns {object}
|
||||
*/
|
||||
chanHistory(channel, from, to){
|
||||
let payload = {
|
||||
channel: channel,
|
||||
from: from
|
||||
}
|
||||
if(to) payload['to'] = to
|
||||
return(this.requestWssGwAction('CHANHIST', payload))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to match a chan with globbing
|
||||
*
|
||||
* @param {string} myChan (no glob)
|
||||
* @param {string} targetChan (possible glob)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
chanMatch(myChan, targetChan) {
|
||||
let re = new RegExp('^'+targetChan.replace(/\*/g,'(.+)')+'$','g')
|
||||
return(myChan.match(re)!=null)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
receiveFromWorker(e) {
|
||||
var workermsg = e.data;
|
||||
if('event' in workermsg){
|
||||
// event "ReceiveFromServer" is the general case of a message from server, found in data, with its own struct.
|
||||
// other type og event are generated by the worker, about the connection
|
||||
switch(workermsg.event){
|
||||
case 'ReceiveFromServer':
|
||||
this.receiveFromServer(JSON.parse(workermsg.data));
|
||||
break;
|
||||
case 'connected':
|
||||
this.connected = true;
|
||||
if(this.config.debug) console.log('received connected event from worker !');
|
||||
this.executewhenConnectedQ();
|
||||
app.events.trigger('MessageBus.Connected');
|
||||
break;
|
||||
|
||||
case 'closed':
|
||||
if(this.config.debug) console.log('received closed event from worker!');
|
||||
this.activeSubscriptions = [];
|
||||
this.callBacksRegister = { };
|
||||
this.whenConnectedQ = [];
|
||||
this.connected = false;
|
||||
app.events.trigger('MessageBus.Closed');
|
||||
break;
|
||||
default:
|
||||
if(this.config.debug) console.warn('Unknown Websocket Worker message:', workermsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} data
|
||||
* @param {string} data.action Possible values: 'SUB', 'SUBLST', ...
|
||||
* @param {string} [data.reqid]
|
||||
* @param {object} [data.payload] response payload
|
||||
* @param {object} [data.err] response error
|
||||
* @param {boolean} [data.success]
|
||||
*/
|
||||
receiveFromServer(srvdata) {
|
||||
// See protocol reminder comment at the bottom
|
||||
if('action' in srvdata){ // Reply to a request
|
||||
let action = srvdata.action;
|
||||
let payload = ('payload' in srvdata) ? srvdata.payload : null;
|
||||
// Piggyback on the results of some actions for this module internal use
|
||||
switch(action){
|
||||
case 'SUB':
|
||||
if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload);
|
||||
break;
|
||||
case 'SUBLST':
|
||||
if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload);
|
||||
break;
|
||||
}
|
||||
app.events.trigger('MessageBus.anyAction', srvdata);
|
||||
} else { // Low-level event : Redis Event, contrary to requ/reply with wssGateway, or other later
|
||||
if(('event' in srvdata) && (srvdata.event == 'REDISMSG')){
|
||||
var payload = ('payload' in srvdata) ? srvdata.payload : null;
|
||||
if(payload && payload.msg && (payload.msg.eventType || payload.msg.action)) {
|
||||
if(payload.msg.eventType){
|
||||
var eventType = payload.msg.eventType;
|
||||
app.events.trigger('MessageBus.event.'+eventType, {
|
||||
chan: payload.chan,
|
||||
sender: payload.msg.sender,
|
||||
eventType: payload.msg.eventType,
|
||||
payload: payload.msg.payload,
|
||||
});
|
||||
} else if(payload.msg.action && payload.msg.reqid) {
|
||||
let reqid = payload.msg.reqid;
|
||||
let action = payload.msg.action;
|
||||
let actionPayload = ('payload' in payload.msg) ? payload.msg.payload : null;
|
||||
let err = ('err' in payload.msg) ? payload.msg.err : null;
|
||||
let success = payload.msg.success;
|
||||
if(reqid in this.promisesRegister) {
|
||||
clearTimeout(this.promisesRegister[reqid][2]); // Stop timeout timer
|
||||
if(success) this.promisesRegister[reqid][0](actionPayload); // resolve
|
||||
else this.promisesRegister[reqid][1](`MsgBus action "${action}" failed.\nError: ${err}`); // Fail
|
||||
}
|
||||
}
|
||||
app.events.trigger('MessageBus.anyMessage', {
|
||||
chan: payload.chan,
|
||||
msg : payload.msg,
|
||||
});
|
||||
} else if(payload && payload.bmsg){
|
||||
app.events.trigger('MessageBus.promiscuousMessage', { // Repill msg : decapsulate & use spcific event
|
||||
chan: payload.bmsg.chan,
|
||||
msg : payload.bmsg.msg,
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.warn('Weird bus message (discarted) :', srvdata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For request-reply, settle promise
|
||||
if(srvdata.reqid && (srvdata.reqid in this.promisesRegister)) {
|
||||
let payload = ('payload' in srvdata) ? srvdata.payload : null;
|
||||
let err = ('err' in srvdata) ? srvdata.err : null;
|
||||
let success = srvdata.success;
|
||||
clearTimeout(this.promisesRegister[srvdata.reqid][2]); // Stop timeout timer
|
||||
if(success) this.promisesRegister[srvdata.reqid][0](payload); // resolve
|
||||
else this.promisesRegister[srvdata.reqid][1](`MsgBus action failed.\nError: ${err}`); // Fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerClass('MessageBus', MessageBus);
|
||||
Executable
+223
@@ -0,0 +1,223 @@
|
||||
'use strict'
|
||||
// Remember : the whole app context is in another parallel & inacessible universe !
|
||||
|
||||
if(typeof(crypto.randomUUID)!='function'){
|
||||
crypto.randomUUID = ()=>{ var buf = new Uint8Array(14);
|
||||
crypto.getRandomValues(buf);
|
||||
var uuid = Array.from(buf, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
||||
return(uuid.substr(0,8)+'-'+uuid.substr(10,4)+'-'+uuid.substr(14,4)+'-'+uuid.substr(18,4)+'-'+uuid.substr(22));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Nicolas Stein
|
||||
* @category Core
|
||||
* @subcategory Libraries
|
||||
* @requires MessageBus
|
||||
*/
|
||||
class MessageBusWorker {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} config
|
||||
* @param {*} userInfo
|
||||
*/
|
||||
constructor(config, userInfo){
|
||||
this.config = config;
|
||||
this.userInfo = userInfo;
|
||||
this.wsurl = this.config.protocol+this.config.hostname;
|
||||
if(('port' in this.config) && (this.config.port!='')) this.wsurl += ':'+this.config.port;
|
||||
this.wsurl += this.config.path ;
|
||||
this.keepAlive = true;
|
||||
this.curReconnectTime = 0;
|
||||
this.ConnectTimeout = null
|
||||
this.token = false
|
||||
this.stateMachine = 'DISCONNECTED'
|
||||
this.noReconnect = false
|
||||
// 'DISCONNECTED'
|
||||
// -> 'LOGIN' (receive challenge & answer to it)
|
||||
// -> 'READY' (received logged=true)
|
||||
this.getToken()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getToken() {
|
||||
if(!this.config.devotpToken){
|
||||
fetch(this.config.tokenUrl+'?'+crypto.randomUUID(),{
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json(), (err => {console.log('ERROR IN FETCH:',err)}))
|
||||
.then(data => {
|
||||
if(data.success && data.payload && data.payload.token) {
|
||||
this.token = data.payload.token
|
||||
if(this.config.debug && ''=='sensitive-even-for-debug') console.log(`Received Token : ${this.token}`)
|
||||
this.connect();
|
||||
} else {
|
||||
console.warn('Could not get messagebus token !')
|
||||
//TODO retry once in a while / integrate in the whole connect process
|
||||
// to be part of retrials...
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.warn('!!! Using dev token for bus !!!')
|
||||
this.token = this.config.devotpToken
|
||||
this.connect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
connect(){
|
||||
this.socket = new WebSocket(this.wsurl);
|
||||
this.ConnectTimeout = setTimeout(() => {
|
||||
if((this.socket) && (close in this.socket)) this.socket.close(null);
|
||||
}, this.config.connectTimeout*1000);
|
||||
this.socket.onopen = this.WSonOpen.bind(this);
|
||||
this.socket.onmessage = this.WSonMessage.bind(this);
|
||||
this.socket.onclose = this.WSonClose.bind(this);
|
||||
this.socket.onerror = this.WSonError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} data
|
||||
*/
|
||||
clientActionDispatch(data){
|
||||
if(this.socket.readyState != 1) {
|
||||
var state = [ 'Connecting', '', 'Closing', 'Closed'];
|
||||
console.warn(`Attempt to send to ${state[this.socket.readyState]} Websocket !`);
|
||||
return;
|
||||
}
|
||||
if(typeof(data)!='string') data=JSON.stringify(data);
|
||||
this.socket.send(data);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e
|
||||
*/
|
||||
WSonOpen(e){
|
||||
this.stateMachine = 'LOGIN'
|
||||
clearTimeout( this.ConnectTimeout);
|
||||
console.log('Websocket connection established');
|
||||
this.curReconnectTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} challenge
|
||||
*/
|
||||
async login(challenge) {
|
||||
let data = new TextEncoder().encode(this.token+challenge)
|
||||
let bytesBuf = await crypto.subtle.digest("SHA-512", data)
|
||||
let arrayBuf = Array.from(new Uint8Array(bytesBuf))
|
||||
let response = arrayBuf.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||
if(this.config.debug && ''=='sensitive-even-for-debug') console.log(`Answering to challenge, with userinfo:`, response, this.userInfo)
|
||||
this.clientActionDispatch({'action':'LOGIN', 'userInfo': this.userInfo , 'otp': response});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e
|
||||
*/
|
||||
WSonMessage(e){
|
||||
if(e.data.toLowerCase()=='unauthorized'){ // Do not spam if session is lost
|
||||
this.noReconnect = true
|
||||
if(this.config.debug) console.log(`Received MSG unauthorized !?`)
|
||||
return;
|
||||
}
|
||||
|
||||
// We're supposed to receive JSON only !
|
||||
try{
|
||||
var data = JSON.parse(e.data);
|
||||
} catch(e){
|
||||
console.warn('WSS: Received garbage :'+e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
//if(this.config.debug) console.log(`Received MSG (in state:${this.stateMachine}) :`, data, this.stateMachine)
|
||||
// LOGIN messages
|
||||
if(this.stateMachine == 'LOGIN'){
|
||||
if(data.action!='LOGIN') { // Non LOGIN messages in a LOGIN state are garbage
|
||||
console.warn('WSS: Non-login message in a LOGIN state',data.action)
|
||||
return
|
||||
}
|
||||
if(data.challenge) { // step1: challenge to reply
|
||||
if(this.config.debug && ''=='sensitive-even-for-debug') console.log(`Got challenge ${data.challenge}...`)
|
||||
this.login(data.challenge)
|
||||
return
|
||||
} else if(data.logged===true){ // step2 logged !
|
||||
if(this.config.debug) console.log(`Logged !`)
|
||||
this.stateMachine = 'READY'
|
||||
postMessage({'event': 'connected' });
|
||||
return
|
||||
} else if(data.logged===false){ // step2 bad login !
|
||||
this.noReconnect = true
|
||||
console.warn('WSS-Login: challenge-response refused. (session lost?)')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if((data.action=='PING') && this.keepAlive){ // Keep Alive is managed here
|
||||
this.clientActionDispatch({'action':'PONG'});
|
||||
return
|
||||
}
|
||||
|
||||
// All other messages are the upper-layer's business !
|
||||
postMessage({'event': 'ReceiveFromServer', 'data':e.data});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e
|
||||
*/
|
||||
WSonClose(e){
|
||||
clearTimeout( this.ConnectTimeout);
|
||||
console.warn(`Websocket connection has closed ! [${(new Date()).toISOString()}]`);
|
||||
postMessage({'event': 'closed' });
|
||||
this.socket.close();
|
||||
if(this.noReconnect) return
|
||||
|
||||
var reconnectTime = parseFloat(this.config.autoReconnect);
|
||||
var reconnectTimeFactor = parseFloat(this.config.autoReconnectTimeFactor);
|
||||
var reconnectTimeMax = parseFloat(this.config.autoReconnectTimeMax);
|
||||
var reconnectJitterPercent = parseFloat(this.config.autoReconnectJitterPercent);
|
||||
if( (!isNaN(reconnectTime)) && (!isNaN(reconnectTimeFactor)) && (!isNaN(reconnectTimeMax)) && (!isNaN(reconnectJitterPercent)) ) {
|
||||
if(this.curReconnectTime==0) this.curReconnectTime = reconnectTime;
|
||||
else {
|
||||
this.curReconnectTime *= reconnectTimeFactor;
|
||||
if(this.curReconnectTime>reconnectTimeMax) this.curReconnectTime = reconnectTimeMax;
|
||||
}
|
||||
var rjit = (Math.random()*reconnectJitterPercent)-(reconnectJitterPercent/2);
|
||||
this.curReconnectTime += (this.curReconnectTime*(rjit/100));
|
||||
// Reconnect in curReconnectTime (=>getToken THEN connect)
|
||||
setTimeout(this.getToken.bind(this), Math.floor(1000*this.curReconnectTime));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e
|
||||
*/
|
||||
WSonError(e){
|
||||
//console.warn('Websocket error:', e.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
var msgbus = null;
|
||||
onmessage = (e) => { // message from client
|
||||
if (e.data.action=='start') {
|
||||
if(!msgbus) msgbus = new MessageBusWorker(e.data.config, e.data.userInfo);
|
||||
} else {
|
||||
if(msgbus) msgbus.clientActionDispatch(e.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
'use strict'
|
||||
/**
|
||||
* @author Nicolas Stein
|
||||
* @category Core
|
||||
* @subcategory Libraries
|
||||
* @requires MessageBus
|
||||
*/
|
||||
class Ptp {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} config
|
||||
* @param {*} userInfo
|
||||
*/
|
||||
constructor(config, userInfo){
|
||||
this.config = config
|
||||
this.userInfo = userInfo
|
||||
if(!app.MessageBus) {
|
||||
throw('Ptp depends upon MessageBus which is absent !')
|
||||
}
|
||||
}
|
||||
|
||||
/* uses msgBus to check get remoteUser online browsers
|
||||
Remote UIs let the user accept the invite (and it selects which browser to use for Ptp)
|
||||
Accepting the invite is done by sending back his browserId
|
||||
Remote can only accept invite if Ptp is enabled, & userAgent is compatible.
|
||||
|
||||
Resolves to null or browserId
|
||||
|
||||
*/
|
||||
invite(remoteUser){
|
||||
return(new Promise(
|
||||
|
||||
))
|
||||
}
|
||||
|
||||
/*
|
||||
uses msgBus to make the WRTC initial handshake
|
||||
*/
|
||||
wrHandshake(remoteBrowserId){
|
||||
return(new Promise(
|
||||
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
app.registerClass('Ptp', Ptp);
|
||||
|
||||
Executable
+557
@@ -0,0 +1,557 @@
|
||||
/**
|
||||
____ _____ __ __ ____ ____ ____
|
||||
( _ \ ( _ )( )( )(_ _)( ___)( _ \
|
||||
) _/ )(_)( )(__)( )( )__) ) /
|
||||
(_)\_) (_____)(______) (__) (____)(_)\_) for SPARC
|
||||
By Mike & Nike
|
||||
* This file is part of Sparc by Mike & Nike.
|
||||
* Sparc is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License,
|
||||
* as published by the Free Software Foundation,
|
||||
* either version 3 of the License, or (at your option) any later version.
|
||||
* Sparc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
* Get your copy of the GNU General Public License at <https://www.gnu.org/licenses/>.
|
||||
* This class contains the SPA router mechanics.<br>
|
||||
* Once instanciated with a set of top-level routes, just launch instance.route().<br>
|
||||
*
|
||||
* @category Core
|
||||
* @subcategory Libraries
|
||||
*/
|
||||
|
||||
class Router {
|
||||
|
||||
/**
|
||||
* @typedef RouteNode
|
||||
* @property {string} url
|
||||
* @property {string} role
|
||||
* @property {string} controller
|
||||
* @property {string} [method]
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} args - object defining top-level routes
|
||||
* @param {object} args.routes - object defining top-level routes
|
||||
* @param {string|function} args.role - user role, used for selecting most apropriate route (if function: called at each re-routing)
|
||||
* @param {function} args.callback - Called when everything a route has been executed
|
||||
* @param {string} args.origin - [Experimental] - when router is not master of the domain root : process only URLs that begin with it.
|
||||
* @param {string} args.controllersPath - Location of application's scripts
|
||||
* @param {object} args.defaults - default values
|
||||
*/
|
||||
constructor(args) {
|
||||
this.defaults = args.defaults;
|
||||
|
||||
if(args.hasOwnProperty('controllersPath') && (args.controllersPath!='')) this.defaults.controllersPath = args.controllersPath;
|
||||
if(args.hasOwnProperty('modelsPath') && (args.modelsPath!='')) this.defaults.modelsPath = args.modelsPath;
|
||||
if(args.hasOwnProperty('viewsPath') && (args.viewsPath!='')) this.defaults.viewsPath = args.viewsPath;
|
||||
this.defaults.origin = args.hasOwnProperty('origin') && (args.origin!='') ? args.origin : (new URL(document.location)).origin;
|
||||
|
||||
this.ControllerInstances = {};
|
||||
this.ControllerConfigs = {};
|
||||
this.httpErrorRoute = null;
|
||||
this.routes = this.cleanupRoutes(args.routes);
|
||||
this.roles = args.roles;
|
||||
this.callback = args.callback;
|
||||
this.routeReady = false;
|
||||
this.merged_ctrl_routes = [];
|
||||
this.currentRoute = null;
|
||||
|
||||
document.addEventListener('click', this.onGlobalClick.bind(this));
|
||||
window.addEventListener('popstate', this.bckFwdInterceptor.bind(this));
|
||||
}
|
||||
|
||||
onGlobalClick(event) {
|
||||
let target = event.target;
|
||||
let notfound = true;
|
||||
while(target && target.tagName && (target.tagName.toUpperCase() != 'BODY') && notfound){
|
||||
if(target.tagName.toUpperCase() == 'A'){
|
||||
notfound = false;
|
||||
this.linksInterceptor(target, event);
|
||||
} else target = target.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
bckFwdInterceptor(e) { this.route(); }
|
||||
|
||||
linksInterceptor(target, e){
|
||||
if(target.hasAttribute('noroute')) return
|
||||
if(target.href.substr(0,this.defaults.origin.length) == this.defaults.origin) { //internal
|
||||
e.preventDefault()
|
||||
history.pushState({}, '', document.location)
|
||||
history.replaceState(null, '', this.cleanupUrl(target.href).pathname)
|
||||
this.route()
|
||||
}
|
||||
}
|
||||
|
||||
cleanupUrl(target) {
|
||||
let url = new URL(target || document.location, this.defaults.origin);
|
||||
url.pathname = url.pathname.replace(/\/+/g,'/');
|
||||
return(url);
|
||||
}
|
||||
|
||||
route(forcedUrl, forcedParams) {
|
||||
if(forcedUrl) history.replaceState(null, null, forcedUrl);
|
||||
|
||||
let routeinfo = this.getRoute(forcedUrl, forcedParams || {});
|
||||
|
||||
let [currentRoute, ctrlPath, ctrlClass, ctrlMethod, params, exturl] = routeinfo
|
||||
if(exturl && exturl.match(/\w{2,6}:\/\//)) {
|
||||
if(params) exturl += '?' +(new URLSearchParams(params).toString());
|
||||
document.location = exturl;
|
||||
this.routeReady = true;
|
||||
return;
|
||||
} else if(exturl && (exturl != '')) { // Internal redirect
|
||||
history.replaceState(null, '', exturl);
|
||||
this.route();
|
||||
} else {
|
||||
this.execRoute(...routeinfo);
|
||||
}
|
||||
}
|
||||
|
||||
execRoute(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params, exturl){
|
||||
this.routeReady = false;
|
||||
|
||||
// Start by making sure we have the MasterController (aka "mc") loaded, if not, first, load it!
|
||||
let mcPath = app.config.router.controllersPath;
|
||||
let mcLoader;
|
||||
|
||||
if(!(app.config.router.masterController in app.LoadedClasses)){
|
||||
mcLoader = app.LoadedClasses.Assets.loadJson({
|
||||
'path': mcPath,
|
||||
'name': app.config.router.masterController+'.json',
|
||||
}).then((tcConfig) => {
|
||||
if(typeof(tcConfig)!='undefined') {
|
||||
tcConfig.routes=[]; // no subroutes for the masterController (use baseRoutes instead)
|
||||
// Avoid controllerConfigLoaded (made for normal ctrl, but merge config directly)
|
||||
this.mergeControllerConfig(currentRoute, mcPath, app.config.router.masterController, null, {} , tcConfig);
|
||||
// return the autoLoadController Promise, so the "then" below depends on ctrl loaded (not config loaded)
|
||||
return(this.autoLoadController(currentRoute, mcPath, app.config.router.masterController, null, {}));
|
||||
} else {
|
||||
console.error('Problem loading Master-Controller ! (could not load its config)');
|
||||
return(new Promise((resolve,fail) => { fail(); }));
|
||||
}
|
||||
});
|
||||
} else { mcLoader = new Promise((resolve) => { resolve(); }) }
|
||||
|
||||
mcLoader.then( () => {
|
||||
// Get controller config file (add controllersPath only to json, for the script it is done by getRoute)
|
||||
app.LoadedClasses.Assets.loadJson({
|
||||
'path': ctrlPath,
|
||||
'name': ctrlClass+'.json',
|
||||
})
|
||||
.then( (config) => {
|
||||
if(typeof(config)!='undefined') {
|
||||
this.controllerConfigLoaded(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params, exturl, config);
|
||||
} else {
|
||||
let err = 'Could not load configuration of controller '+ctrlClass+' !!';
|
||||
this.redirectToHttpErrorRoute(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
controllerConfigLoaded(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params, exturl, config) {
|
||||
config = config || { // Cool attitude if config not found or not clean json
|
||||
routes:[],
|
||||
dependencies:[],
|
||||
AssetsDependencies:{}
|
||||
};
|
||||
|
||||
this.mergeControllerConfig(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params, config);
|
||||
|
||||
// Recompute route now that there are some sub-routes, if something changed we need to recurse
|
||||
// If nothing changed, then we're good to load, instanciate & call !
|
||||
let newCurrentRoute, newCtrlPath, newCtrlClass, newCtrlMethod, newParams;
|
||||
[newCurrentRoute, newCtrlPath, newCtrlClass, newCtrlMethod, newParams, exturl] = this.getRoute(currentRoute.forcedUrl, currentRoute.forcedParams);
|
||||
|
||||
if(exturl) { //Do not recur if we stumbbled on a exturl, just go to it
|
||||
this.route(exturl, null)
|
||||
} else if(newCurrentRoute.url!=currentRoute.url){
|
||||
this.execRoute(newCurrentRoute, newCtrlPath, newCtrlClass, newCtrlMethod, newParams, exturl);
|
||||
} else { // the else makes sure we autoLoad only at the bottom-level of the recursion
|
||||
if(typeof(this.ControllerInstances[ctrlClass])=='object') {
|
||||
// Non-persistent controllers were once were here, but not usefull, removed
|
||||
this.launchControllerMethod(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params);
|
||||
} else { // Unknown ctrl : load it !
|
||||
this.autoLoadController(newCurrentRoute, newCtrlPath, newCtrlClass, newCtrlMethod, newParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mergeControllerConfig(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params, config){
|
||||
this.ControllerConfigs[ctrlClass] = config
|
||||
|
||||
// Carefull not to remerge twice the same config (otherwise url concats will go bzurg)
|
||||
if(this.merged_ctrl_routes.indexOf(ctrlClass)>-1) return;
|
||||
this.merged_ctrl_routes.push(ctrlClass);
|
||||
|
||||
// Add sub routes, except for masterctrl
|
||||
if((ctrlClass!= app.config.router.masterController) || ('routes' in config)){
|
||||
let newroutes = this.cleanupRoutes(config.routes);
|
||||
for(let newroute of newroutes){
|
||||
if(currentRoute.url=='!defaultroute') continue;
|
||||
let fullurl = (currentRoute.url+'/'+newroute.url).replace(/\/+/g,'/');
|
||||
newroute.url = fullurl;
|
||||
this.routes.push(newroute);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Loader.Dependencies.hasOwnProperty(ctrlClass)) Loader.Dependencies[ctrlClass] = [];
|
||||
// Add controller dependencies (not views, not)
|
||||
if('controllerDependencies' in config){
|
||||
for(var script of config.controllerDependencies){
|
||||
if(script.substring(0,4)!='http') Loader.Dependencies[ctrlClass].push(('/app/'+script).replace(/\/+/g,'/'))
|
||||
else Loader.Dependencies[ctrlClass].push(script)
|
||||
}
|
||||
}
|
||||
|
||||
// add models as controller dependencies with adapted path
|
||||
if('models' in config){
|
||||
for(var dep of config.models){
|
||||
if(typeof(dep) == 'string') {// just add model as dependency of controller, with correct path
|
||||
let model = (this.defaults.modelsPath+'/'+dep).replace(/\/+/g,'/');
|
||||
Loader.Dependencies[ctrlClass].push(model);
|
||||
} else if(typeof(dep) == 'object'){ // This model depends on other models
|
||||
let model = (this.defaults.modelsPath+'/'+dep.model).replace(/\/+/g,'/');
|
||||
// add model as dependency of controller
|
||||
Loader.Dependencies[ctrlClass].push(model);
|
||||
// add sub-dependencies without dups & with correct path
|
||||
if(!Loader.Dependencies.hasOwnProperty(model)) Loader.Dependencies[model] = [];
|
||||
for(let submodel of dep.dependencies){
|
||||
if(Loader.Dependencies[model].indexOf(submodel)<0) {
|
||||
Loader.Dependencies[model].push((this.defaults.modelsPath+'/'+submodel).replace(/\/+/g,'/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add views as controller dependencies with adapted path
|
||||
if('views' in config){
|
||||
for(var dep of config.views){
|
||||
if(typeof(dep) == 'string') {// just add view as dependency of controller, with correct path
|
||||
let view = (this.defaults.viewsPath+'/'+dep).replace(/\/+/g,'/');
|
||||
Loader.Dependencies[ctrlClass].push(view);
|
||||
} else if(typeof(dep) == 'object'){ // This view depends on other views
|
||||
let view = (this.defaults.viewsPath+'/'+dep.view).replace(/\/+/g,'/');
|
||||
// add view as dependency of controller
|
||||
Loader.Dependencies[ctrlClass].push(view);
|
||||
// add sub-dependencies without dups & with correct path
|
||||
if(!Loader.Dependencies.hasOwnProperty(view)) Loader.Dependencies[view] = [];
|
||||
for(let subview of dep.dependencies){
|
||||
if(Loader.Dependencies[view].indexOf(subview)<0) {
|
||||
Loader.Dependencies[view].push((this.defaults.viewsPath+'/'+subview).replace(/\/+/g,'/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove dups (maybe there was existing stuff before we got here)
|
||||
Loader.Dependencies[ctrlClass] = Array.from(new Set(Loader.Dependencies[ctrlClass]));
|
||||
Loader.AssetsDependencies[ctrlClass] = config.assets;
|
||||
}
|
||||
|
||||
autoLoadController(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params){
|
||||
let scriptFp = (ctrlPath+'/'+ctrlClass).replace(/\/+/g,'/');
|
||||
let deps = {}; deps[scriptFp] = Loader.Dependencies[ctrlClass];
|
||||
return (
|
||||
Loader.loadScripts({
|
||||
'scripts':[scriptFp],
|
||||
'dependencies':deps,
|
||||
}).then(
|
||||
() => {
|
||||
this.controllerLoaded(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params)
|
||||
},
|
||||
(error) => { // Loading of the route failed => Try to reroute to defaultroute
|
||||
let err = 'Loading of the class "'+ctrlClass+'" Failed !';
|
||||
console.error(err);
|
||||
if(currentRoute != this.httpErrorRoute) this.redirectToHttpErrorRoute(err);
|
||||
this.routeReady = true;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
registerController(ctrlClass){
|
||||
try {
|
||||
this.ControllerInstances[ctrlClass] = new app.LoadedClasses[ctrlClass]();
|
||||
} catch(e) { console.error(`"${e}" instantiating controller ${ctrlClass} `); } ;
|
||||
}
|
||||
|
||||
controllerLoaded(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params){
|
||||
|
||||
this.registerController(ctrlClass);
|
||||
|
||||
if(ctrlClass != app.config.router.masterController) {
|
||||
let mTemplate
|
||||
if('template' in currentRoute)
|
||||
mTemplate = currentRoute.template;
|
||||
else
|
||||
mTemplate = app.config.router.defaultMasterTemplate;
|
||||
|
||||
// Launch the useTemplate and wait for it to be ready before launching the method.
|
||||
this.ControllerInstances[app.config.router.masterController].useTemplate(mTemplate).then(
|
||||
() => {
|
||||
this.launchControllerMethod(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
launchControllerMethod(currentRoute, ctrlPath, ctrlClass, ctrlMethod, params){
|
||||
if((ctrlClass in this.ControllerInstances) && (typeof(this.ControllerInstances[ctrlClass][ctrlMethod])=='function')) {
|
||||
this.currentRoute = currentRoute
|
||||
this.currentRoute.realUrl = new URL(document.location.href).pathname
|
||||
this.ControllerInstances[ctrlClass][ctrlMethod]({
|
||||
'currentRoute': currentRoute,
|
||||
'params': params
|
||||
});
|
||||
this.routeReady = true;
|
||||
if(typeof(this.callback)=='function') this.callback(ctrlClass,ctrlMethod,params);
|
||||
} else {
|
||||
let err;
|
||||
if(ctrlClass in this.ControllerInstances){
|
||||
err = `Could not find method "${ctrlMethod}" in class "${ctrlClass}"`;
|
||||
} else {
|
||||
err = `Controller "${ctrlClass}" not instantiated !`;
|
||||
}
|
||||
console.error(err);
|
||||
if(currentRoute != this.httpErrorRoute)
|
||||
this.redirectToHttpErrorRoute(err);
|
||||
this.routeReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
redirectToHttpErrorRoute(err=''){
|
||||
let ctrlPath = this.httpErrorRoute.controller.substr(0,this.httpErrorRoute.controller.lastIndexOf('/')+1);
|
||||
ctrlPath = (this.defaults.controllersPath+'/'+ctrlPath).replace(/\/+/g,'/');
|
||||
let ctrlClass = this.httpErrorRoute.controller.substr(this.httpErrorRoute.controller.lastIndexOf('/')+1);
|
||||
let ctrlMethod =this.httpErrorRoute.method;
|
||||
app.LoadedClasses.Assets.loadJson({
|
||||
'path': ctrlPath,
|
||||
'name': ctrlClass+'.json',
|
||||
}).then( (config) => {
|
||||
this.mergeControllerConfig(this.httpErrorRoute, ctrlPath, ctrlClass, ctrlMethod, {'error' : err}, config)
|
||||
this.autoLoadController(this.httpErrorRoute, ctrlPath, ctrlClass, ctrlMethod, {'error' : err});
|
||||
});
|
||||
}
|
||||
|
||||
getRoute(forcedUrl, forcedParams) {
|
||||
let pathname = this.cleanupUrl(forcedUrl).pathname;
|
||||
let bestroute = {'score':0, 'route':{}, 'parts':'', 'extractedParams':{}, 'gibberish':'', idx:0 };
|
||||
let idx = 0;
|
||||
let myRoles = (typeof(this.roles)=='function') ? this.roles() : this.roles ;
|
||||
for(let route of this.routes){
|
||||
route['forcedUrl'] = forcedUrl;
|
||||
route['forcedParams'] = forcedParams;
|
||||
let intersect = route['role'].filter(v => (myRoles.indexOf(v)>-1));
|
||||
if((route['role']!='*') && (intersect.length==0)) continue;
|
||||
|
||||
let [score, parts, paramvals, gibberish] = this.matchUrl(pathname, route);
|
||||
|
||||
// make sure a role specific route wins over a '*' role route (if already match, thus score>0, not to interfere with defaultroute).
|
||||
if((score>0) && (route['role']!='*') && (intersect.length>0)) {
|
||||
score++;
|
||||
}
|
||||
|
||||
// Best route wins, and when exaequo, childe-route (embed) wins over a parent-route
|
||||
if( (score > bestroute.score) || ((score == bestroute.score) && (idx > bestroute.idx)) ) {
|
||||
bestroute = {'score':score, 'route':route, 'parts':parts, 'extractedParams':paramvals, 'gibberish':gibberish, idx:idx };
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
if(bestroute['score']==0){ // no match => return default route
|
||||
if(this.httpErrorRoute)
|
||||
bestroute = {
|
||||
'score': 0,
|
||||
'route': this.httpErrorRoute,
|
||||
'parts': [],
|
||||
'extractedParams': {'error' : 'No route found for this URL!'} ,
|
||||
'gibberish': pathname,
|
||||
'idx': 0
|
||||
};
|
||||
else {
|
||||
console.error('No matching route for this url, and no default route defined!');
|
||||
return([null,'', '', '', {}]);
|
||||
}
|
||||
}
|
||||
|
||||
let ctrlPath, ctrlClass, ctrlMethod, methParams, exturl;
|
||||
if(bestroute.route.hasOwnProperty('controller')){ // Normal MVC route
|
||||
ctrlPath = bestroute.route.controller.substr(0,bestroute.route.controller.lastIndexOf('/')+1);
|
||||
ctrlPath = (this.defaults.controllersPath+'/'+ctrlPath).replace(/\/+/g,'/');
|
||||
ctrlClass = bestroute.route.controller.substr(bestroute.route.controller.lastIndexOf('/')+1);
|
||||
if(bestroute.route.hasOwnProperty('method') && (bestroute.route.method!='')){ // predefined Method in config
|
||||
ctrlMethod =bestroute.route.method;
|
||||
} else { // no predefined Method,
|
||||
// unused stuff in url ? use last url part
|
||||
if(bestroute.score >= 2) ctrlMethod = bestroute.parts[bestroute.parts.length-1];
|
||||
else ctrlMethod = 'index'; // nothing left in url ? default to index
|
||||
}
|
||||
|
||||
if(!bestroute.route.hasOwnProperty('extractedParams')) bestroute.params = {};
|
||||
methParams = {...bestroute.extractedParams, ...bestroute.route.params, ...forcedParams};
|
||||
exturl = '';
|
||||
} else { // External route
|
||||
exturl = bestroute.route.exturl;
|
||||
}
|
||||
return([bestroute.route, ctrlPath, ctrlClass, ctrlMethod, methParams, exturl]);
|
||||
}
|
||||
|
||||
cleanupRoutes(routes){
|
||||
const regex = /[\/:][^\/:]+/g;
|
||||
const paramregex = /^:(\w+)(\(.*\))?/;
|
||||
const pathregex = /^[-\/\w]+$/;
|
||||
const roleregex = /^[-\w]+$/;
|
||||
let cleanroutes = [];
|
||||
for(let route of routes){
|
||||
let keepit = true;
|
||||
|
||||
if(!route.hasOwnProperty('url')) {
|
||||
console.warn('Missing url in route definition, route ignored ! ',route);
|
||||
keepit = false;
|
||||
} else {
|
||||
if(route.url!='!defaultroute'){
|
||||
let routeparts = route.url.match(regex);
|
||||
if(routeparts===null) routeparts = ['/']; //Happens for empty of slah-only url; (or maybe cataclysmic ones)
|
||||
for(let routefrag of routeparts) {
|
||||
if(routefrag.charAt(0)=='/'){
|
||||
if(!(routefrag.match(pathregex))) {
|
||||
console.warn('Bad url in route definition (forbidden character in path), route ignored ! ',route);
|
||||
keepit = false;
|
||||
}
|
||||
} else {
|
||||
if(!(routefrag.match(paramregex))) {
|
||||
console.warn('Bad url in route definition (forbidden character in param), route ignored ! ',route);
|
||||
keepit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.httpErrorRoute = route;
|
||||
}
|
||||
}
|
||||
|
||||
if(!route.hasOwnProperty('role')) {
|
||||
console.warn('Missing role in route definition, route ignored ! ',route);
|
||||
keepit = false;
|
||||
} else {
|
||||
if(typeof(route.role)=='string') route.role = [route.role];
|
||||
if(route.role!='*'){
|
||||
for(let role of route.role){ // also works if string (letter by letter but ok)
|
||||
if(!(role.match(roleregex))) {
|
||||
console.warn('Bad role in route definition (forbidden character), route ignored ! ',route);
|
||||
keepit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( (!route.hasOwnProperty('controller')) && (!route.hasOwnProperty('exturl')) ) {
|
||||
console.warn('Missing one of controller / exturl in route definition, route ignored ! ',route);
|
||||
keepit = false;
|
||||
} else if(route.hasOwnProperty('exturl')) {
|
||||
// don't touch it !
|
||||
} else if(!(route.controller.match(pathregex))) {
|
||||
console.warn('Bad controller path in route definition (forbidden character), route ignored ! ',route);
|
||||
keepit = false;
|
||||
}
|
||||
if(keepit) cleanroutes.push(route);
|
||||
}
|
||||
return(cleanroutes);
|
||||
}
|
||||
|
||||
matchUrl(urlpath, route){
|
||||
const regex = /[\/:][^\/:]+/g;
|
||||
const paramregex = /:(\w+)(\(.*\))?/;
|
||||
|
||||
if((urlpath=='/') && (route.url=='/')) return([1, '/', {}, []]);
|
||||
|
||||
let urlpaths = urlpath.substr(1).split('/');
|
||||
let nbmatch = 0;
|
||||
let gibberish = '';
|
||||
let routeparts;
|
||||
routeparts = route.url.match(regex);
|
||||
let parts = []; let params = {};
|
||||
if(!routeparts) return([0, [], {}, '']);
|
||||
|
||||
for(let routefrag of routeparts) {
|
||||
if(nbmatch>(urlpaths.length-1)) {
|
||||
return([0, [], {}, '']);
|
||||
}
|
||||
if(routefrag.charAt(0)=='/'){ // url frag
|
||||
if(routefrag!='/') routefrag = routefrag.substr(1);
|
||||
if(routefrag!=urlpaths[nbmatch]){
|
||||
return([0, [], {}, '']);
|
||||
}
|
||||
parts.push(urlpaths[nbmatch]);
|
||||
} else { // param
|
||||
if(urlpaths[nbmatch]=='') return([0, [], {}, '']); // no '//' where param expected
|
||||
let parampattern = routefrag.match(paramregex);
|
||||
if(!parampattern) {
|
||||
console.warn('Bad parameter syntax in routes definition : '+routefrag+'(ignored)');
|
||||
} else {
|
||||
let [x, parname, parvalidation] = parampattern
|
||||
if(parvalidation){ //Validate with given regex
|
||||
let pvrx = RegExp('^'+parvalidation.substr(1, parvalidation.length-2)+'$');
|
||||
if(urlpaths[nbmatch].match(pvrx)) params[parname] = urlpaths[nbmatch];
|
||||
else return([0, [], {}, '']); // parameter with missed validation
|
||||
} else {
|
||||
params[parname] = urlpaths[nbmatch]; // take without validation
|
||||
}
|
||||
}
|
||||
}
|
||||
nbmatch++;
|
||||
}
|
||||
|
||||
let pointer;
|
||||
// Still have stuff in url, and no predefined method, so take meth from url
|
||||
if( (nbmatch<urlpaths.length) && ((!route.hasOwnProperty('method') || (route.method==''))) && (urlpaths[nbmatch]!='') ){
|
||||
parts.push(urlpaths[nbmatch]);
|
||||
pointer = nbmatch+1; // Pointer should advance as we've taken this part of the url, but does not count in score !
|
||||
} else pointer = nbmatch;
|
||||
|
||||
if(pointer<urlpaths.length){
|
||||
gibberish = urlpaths.splice(pointer).join('/');
|
||||
}
|
||||
return([nbmatch, parts, params, gibberish]);
|
||||
}
|
||||
|
||||
makelink(ctrl, method, params={}){
|
||||
let myRoles = (typeof(this.roles)=='function') ? this.roles() : this.roles ;
|
||||
for(let route of this.routes){
|
||||
let intersect = route['role'].filter(v => (myRoles.indexOf(v)>-1));
|
||||
if((route.role!='*') && (intersect.length==0)) continue
|
||||
|
||||
if(('/'+route.controller).replace(/\/+/g,'/')!=('/'+ctrl).replace(/\/+/g,'/')) continue
|
||||
let url=route.url;
|
||||
//Check if params and route params match
|
||||
let parok=true;
|
||||
let rparams = route.url.match(/:\w+/g);
|
||||
if(rparams){
|
||||
let re;
|
||||
for(let rparam of rparams){
|
||||
rparam=rparam.substr(1);
|
||||
if(!params.hasOwnProperty(rparam)) {
|
||||
parok=false;
|
||||
break;
|
||||
}
|
||||
re = RegExp(':'+rparam+'(?!\w)','g');
|
||||
url = url.replace(re, '/'+params[rparam]);
|
||||
}
|
||||
}
|
||||
if(!parok) continue;
|
||||
|
||||
if(!route.hasOwnProperty('method')){
|
||||
url+=('/'+method).replace(/\/+/g,'/');
|
||||
}
|
||||
return(url);
|
||||
}
|
||||
return('');
|
||||
}
|
||||
}
|
||||
app.registerClass('Router', Router);
|
||||
|
||||
Reference in New Issue
Block a user