unclean SPARC
This commit is contained in:
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