- fixed some "map sync" bugs with WebSocket installations

- improved WebSocket info on `/setup` page (new live stats, new log viewer,..)
This commit is contained in:
Mark Friedrich
2019-07-02 20:04:16 +02:00
parent 176cd41a49
commit 437fdf0db9
21 changed files with 574 additions and 131 deletions

View File

@@ -25,7 +25,6 @@ class Map extends Controller\AccessController {
// cache keys
const CACHE_KEY_INIT = 'CACHED_INIT';
const CACHE_KEY_MAP_DATA = 'CACHED.MAP_DATA.%s';
const CACHE_KEY_USER_DATA = 'CACHED.USER_DATA.%s';
const CACHE_KEY_HISTORY = 'CACHED_MAP_HISTORY_%s';
@@ -652,6 +651,7 @@ class Map extends Controller\AccessController {
protected function broadcastMapAccess(Pathfinder\MapModel $map){
$mapAccess = [
'id' => $map->_id,
'name' => $map->name,
'characterIds' => array_map(function ($data){
return $data->id;
}, $map->getCharactersData())
@@ -690,17 +690,18 @@ class Map extends Controller\AccessController {
}
$return->data = [
'id' => $activeCharacter->_id,
'token' => bin2hex(random_bytes(16)), // token for character access
'id' => $activeCharacter->_id,
'token' => bin2hex(random_bytes(16)), // token for character access
'characterData' => $characterData,
'mapData' => []
'mapData' => []
];
if($maps){
foreach($maps as $map){
$return->data['mapData'][] = [
'id' => $map->_id,
'token' => bin2hex(random_bytes(16)) // token for map access
'id' => $map->_id,
'token' => bin2hex(random_bytes(16)), // token for map access
'name' => $map->name
];
}
}

View File

@@ -251,7 +251,7 @@ class Sso extends Api\User{
$this->setLoginCookie($characterModel);
// -> pass current character data to target page
$f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA, $characterModel->_id);
$this->setTempCharacterData($characterModel->_id);
// route to "map"
if($rootAlias == 'admin'){

View File

@@ -388,12 +388,12 @@ class Controller {
public function getSessionCharacterData() : array {
$data = [];
if($user = $this->getUser()){
$header = self::getRequestHeaders();
$requestedCharacterId = (int)$header['Pf-Character'];
$header = self::getRequestHeaders();
$requestedCharacterId = (int)$header['Pf-Character'];
if( !$this->getF3()->get('AJAX') ){
$requestedCharacterId = (int)$_COOKIE['old_char_id'];
if(!$requestedCharacterId){
$tempCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA);
$tempCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA);
if((int)$tempCharacterData['ID'] > 0){
$requestedCharacterId = (int)$tempCharacterData['ID'];
}

View File

@@ -1557,23 +1557,29 @@ class Setup extends Controller {
'class' => 'txt-color-danger'
];
$webSocketStatus = [
$statusWeb = [
'type' => 'danger',
'label' => 'INIT CONNECTION…',
'class' => 'txt-color-danger'
];
$statsTcp = [
'startup' => 0,
'connections' => 0,
'maxConnections' => 0
];
$statsTcp = false;
$statsWeb = false;
$setStats = function(array $stats) use (&$statsTcp, &$statsWeb) {
if(!empty($stats['tcpSocket'])){
$statsTcp = $stats['tcpSocket'];
}
if(!empty($stats['webSocket'])){
$statsWeb = $stats['webSocket'];
}
};
// ping TCP Socket with "healthCheck" task
$f3->webSocket(['timeout' => $ttl])
->write($task, $healthCheckToken)
->then(
function($payload) use ($task, $healthCheckToken, &$statusTcp, &$statsTcp) {
function($payload) use ($task, $healthCheckToken, &$statusTcp, $setStats) {
if(
$payload['task'] == $task &&
$payload['load'] == $healthCheckToken
@@ -1581,24 +1587,26 @@ class Setup extends Controller {
$statusTcp['type'] = 'success';
$statusTcp['label'] = 'PING OK';
$statusTcp['class'] = 'txt-color-success';
// statistics (e.g. current connection count)
if(!empty($payload['stats'])){
$statsTcp = $payload['stats'];
}
}else{
$statusTcp['type'] = 'warning';
$statusTcp['label'] = is_string($payload['load']) ? $payload['load'] : 'INVALID RESPONSE';
$statusTcp['class'] = 'txt-color-warning';
}
// statistics (e.g. current connection count)
$setStats((array)$payload['stats']);
},
function($payload) use (&$statusTcp) {
function($payload) use (&$statusTcp, $setStats) {
$statusTcp['label'] = $payload['load'];
// statistics (e.g. current connection count)
$setStats((array)$payload['stats']);
});
$socketInformation = [
'tcpSocket' => [
'label' => 'Socket (intern) [TCP]',
'label' => 'TCP-Socket (intern)',
'icon' => 'fa-exchange-alt',
'status' => $statusTcp,
'stats' => $statsTcp,
'data' => [
@@ -1620,15 +1628,17 @@ class Setup extends Controller {
'check' => !empty( $ttl )
],[
'label' => 'uptime',
'value' => Config::formatTimeInterval($statsTcp['startup']),
'value' => Config::formatTimeInterval($statsTcp['startup'] ? : 0),
'check' => $statsTcp['startup'] > 0
]
],
'token' => $healthCheckToken
],
'webSocket' => [
'label' => 'WebSocket (clients) [HTTP]',
'status' => $webSocketStatus,
'label' => 'Web-Socket',
'icon' => 'fa-random',
'status' => $statusWeb,
'stats' => $statsWeb,
'data' => [
[
'label' => 'URI',

View File

@@ -203,9 +203,9 @@ define([], () => {
* @param version
*/
let showVersionInfo = (version) => {
console.ok('%c PATHFINDER',
'color: #477372; font-size: 25px; margin-left: 10px; line-height: 100px; text-shadow: 1px 1px 0 #212C30; ' +
'background: url(https://i.imgur.com/1Gw8mjL.png) no-repeat;');
console.ok('%c PATHFINDER',
'color: #477372; font-size: 25px; margin-left: 10px; line-height: 50px; text-shadow: 1px 1px 0 #212C30; ' +
'background: url(https://i.imgur.com/bhSr6LI.png) no-repeat;');
console.pf('Release: %s', version);
};

View File

@@ -4,13 +4,9 @@
define([
'app/util'
], function(Util){
], (Util) => {
'use strict';
let config = {
};
let sharedWorker = null;
let MsgWorker = null;
let characterId = null;
@@ -29,17 +25,13 @@ define([
* get SharedWorker Script path
* @returns {string}
*/
let getWorkerScript = () => {
return '/public/js/' + Util.getVersion() + '/app/worker/map.js';
};
let getWorkerScript = () => '/public/js/' + Util.getVersion() + '/app/worker/map.js';
/**
* get path to message object
* @returns {string}
*/
let getMessageWorkerObjectPath = () => {
return '/public/js/' + Util.getVersion() + '/app/worker/message.js';
};
let getMessageWorkerObjectPath = () => '/public/js/' + Util.getVersion() + '/app/worker/message.js';
/**
* init (connect) WebSocket within SharedWorker
@@ -51,14 +43,14 @@ define([
characterId: characterId,
});
sharedWorker.port.postMessage(MsgWorkerInit);
sendMessage(MsgWorkerInit);
};
/**
* init (start/connect) to "SharedWorker"
* init (start/connect) to "SharedWorker" thread
* -> set worker events
*/
let init = (config) => {
let init = config => {
// set characterId that is connected with this SharedWorker PORT
characterId = parseInt(config.characterId);
@@ -67,9 +59,9 @@ define([
MsgWorker = window.MsgWorker;
// start/connect to "SharedWorker"
sharedWorker = new SharedWorker( getWorkerScript(), getMessageWorkerObjectPath() );
sharedWorker = new SharedWorker(getWorkerScript(), getMessageWorkerObjectPath());
sharedWorker.port.addEventListener('message', (e) => {
sharedWorker.port.addEventListener('message', e => {
let MsgWorkerMessage = e.data;
Object.setPrototypeOf(MsgWorkerMessage, MsgWorker.prototype);
@@ -90,7 +82,7 @@ define([
}
}, false);
sharedWorker.onerror = (e) => {
sharedWorker.onerror = e => {
// could not connect to SharedWorker script -> send error back
let MsgWorkerError = new MsgWorker('sw:error');
MsgWorkerError.meta({
@@ -111,17 +103,49 @@ define([
});
};
/**
* send data to "SharedWorker" thread
* @param task
* @param data
*/
let send = (task, data) => {
let MsgWorkerSend = new MsgWorker('ws:send');
MsgWorkerSend.task(task);
MsgWorkerSend.data(data);
sharedWorker.port.postMessage(MsgWorkerSend);
sendMessage(MsgWorkerSend);
};
/**
* send close port task to "SharedWorker" thread
* -> this removes the port from its port collection and closes it
*/
let close = () => {
let MsgWorkerClose = new MsgWorker('sw:closePort');
MsgWorkerClose.task('unsubscribe');
sendMessage(MsgWorkerClose);
};
/**
*
* @param {window.MsgWorker} MsgWorkerSend
*/
let sendMessage = MsgWorkerSend => {
if(sharedWorker instanceof SharedWorker){
if(MsgWorkerSend instanceof window.MsgWorker){
sharedWorker.port.postMessage(MsgWorkerSend);
}else{
console.error('MsgWorkerSend must be instance of window.MsgWorker');
}
}else{
console.error('SharedWorker thread not found');
}
};
return {
getWebSocketURL: getWebSocketURL,
init: init,
send: send
send: send,
close: close
};
});

View File

@@ -221,7 +221,7 @@ define([
* @param payload
* @returns {Promise<any>}
*/
let initMapModule = (payload) => {
let initMapModule = payload => {
let initMapModuleExecutor = (resolve, reject) => {
// init browser tab change observer, Once the timers are available
@@ -248,10 +248,10 @@ define([
* @param payloadMapAccessData
* @returns {Promise<any>}
*/
let initMapWorker = (payloadMapAccessData) => {
let initMapWorker = payloadMapAccessData => {
let initMapWorkerExecutor = (resolve, reject) => {
let getPayload = (command) => {
let getPayload = command => {
return {
action: 'initMapWorker',
data: {
@@ -270,7 +270,7 @@ define([
// init SharedWorker for maps
MapWorker.init({
characterId: response.data.id,
characterId: response.data.id,
callbacks: {
onInit: (MsgWorkerMessage) => {
Util.setSyncStatus(MsgWorkerMessage.command);
@@ -552,6 +552,9 @@ define([
// Send map update request on tab close/reload, in order to save map changes that
// haven´t been saved through default update trigger
window.addEventListener('beforeunload', function(e){
// close connection to "SharedWorker"
MapWorker.close();
// save unsaved map changes ...
triggerMapUpdatePing();

View File

@@ -11,7 +11,9 @@ define([
'use strict';
let config = {
splashOverlayClass: 'pf-splash' // class for "splash" overlay
splashOverlayClass: 'pf-splash', // class for "splash" overlay
webSocketStatsId: 'pf-setup-webSocket-stats', // id for webSocket "stats" panel
webSocketRefreshStatsId: 'pf-setup-webSocket-stats-refresh' // class for "reload stats" button
};
/**
@@ -42,6 +44,18 @@ define([
});
};
/**
*
* @param container
* @param selector
*/
let setCollapseObserver = (container, selector) => {
container.find(selector).css({cursor: 'pointer'});
container.on('click', selector, function(){
$(this).find('.pf-animate-rotate').toggleClass('right');
});
};
/**
* set page observer
*/
@@ -52,9 +66,7 @@ define([
Util.initPageScroll(body);
// collapse ---------------------------------------------------------------------------------------------------
body.find('[data-toggle="collapse"]').css({cursor: 'pointer'}).on('click', function(){
$(this).find('.pf-animate-rotate').toggleClass('right');
});
setCollapseObserver(body, '[data-toggle="collapse"]');
// buttons ----------------------------------------------------------------------------------------------------
// exclude "download" && "navigation" buttons
@@ -128,6 +140,29 @@ define([
}
};
/**
* get WebSockets "subscriptions" <table> HTML
* @param subscriptionStats
* @returns {Promise<any>}
*/
let getWebSocketSubscriptionTable = subscriptionStats => {
let executor = resolve => {
requirejs(['text!templates/modules/subscriptions_table.html', 'mustache'], (template, Mustache) => {
let data = {
panelId: config.webSocketStatsId,
refreshButtonId: config.webSocketRefreshStatsId,
subStats: subscriptionStats,
channelCount: (Util.getObjVal(subscriptionStats, 'channels') || []).length
};
resolve(Mustache.render(template, data));
});
};
return new Promise(executor);
};
/**
* perform a basic check if Clients (browser) can connect to the webSocket server
*/
@@ -158,7 +193,7 @@ define([
let socketDangerCount = parseInt(badgeSocketDanger.text()) || 0;
if(data.uri){
let uriRow = webSocketPanel.find('.panel-body table tr');
let uriRow = webSocketPanel.find('.panel-body').filter(':first').find('table tr');
uriRow.find('td:nth-child(2) kbd').html(data.uri.value);
if(data.uri.status){
let statusIcon = uriRow.find('td:nth-child(3) i');
@@ -206,6 +241,18 @@ define([
}
});
/**
* @param socket
* @param task
* @param load
*/
let sendMessage = (socket, task, load) => {
socket.send(JSON.stringify({
task: task,
load: load
}));
};
// try to connect to WebSocket server
let socket = new WebSocket(webSocketURI);
@@ -219,10 +266,7 @@ define([
});
// sent token and check response
socket.send(JSON.stringify({
task: 'healthCheck',
load: tcpSocketPanel.data('token')
}));
sendMessage(socket, 'healthCheck', tcpSocketPanel.attr('data-token'));
webSocketPanel.hideLoadingAnimation();
};
@@ -230,7 +274,7 @@ define([
socket.onmessage = (e) => {
let response = JSON.parse(e.data);
if(response === 1){
if(Util.getObjVal(response, 'load.isValid') === true){
// SUCCESS
updateWebSocketPanel({
status: {
@@ -239,6 +283,23 @@ define([
class: 'txt-color-success'
}
});
// show subscription stats table
getWebSocketSubscriptionTable(Util.getObjVal(response, 'load.subStats')).then(payload => {
// remove existing table -> then insert new
$('#' + config.webSocketStatsId).remove();
$(payload).insertAfter(webSocketPanel).initTooltips();
let token = Util.getObjVal(response, 'load.token');
tcpSocketPanel.attr('data-token', token);
// set "reload stats" observer
$('#' + config.webSocketRefreshStatsId).on('click', function(){
$('#' + config.webSocketStatsId).showLoadingAnimation();
sendMessage(socket, 'healthCheck', token);
});
});
}else{
// Got response but INVALID
updateWebSocketPanel({
@@ -274,6 +335,8 @@ define([
});
webSocketPanel.hideLoadingAnimation();
$('#' + config.webSocketStatsId).remove();
};
};

View File

@@ -12,14 +12,14 @@ let ports = [];
let characterPorts = [];
// init "WebSocket" connection ========================================================================================
let initSocket = (uri) => {
let initSocket = uri => {
let MsgWorkerOpen = new MsgWorker('ws:open');
if(socket === null){
socket = new WebSocket(uri);
// "WebSocket" open -----------------------------------------------------------------------
socket.onopen = (e) => {
socket.onopen = e => {
MsgWorkerOpen.meta({
readyState: socket.readyState
});
@@ -28,7 +28,7 @@ let initSocket = (uri) => {
};
// "WebSocket message ---------------------------------------------------------------------
socket.onmessage = (e) => {
socket.onmessage = e => {
let response = JSON.parse(e.data);
let MsgWorkerSend = new MsgWorker('ws:send');
@@ -43,7 +43,7 @@ let initSocket = (uri) => {
};
// "WebSocket" close ----------------------------------------------------------------------
socket.onclose = (closeEvent) => {
socket.onclose = closeEvent => {
let MsgWorkerClosed = new MsgWorker('ws:closed');
MsgWorkerClosed.meta({
readyState: socket.readyState,
@@ -57,7 +57,7 @@ let initSocket = (uri) => {
};
// "WebSocket" error ----------------------------------------------------------------------
socket.onerror = (e) => {
socket.onerror = e => {
let MsgWorkerError = new MsgWorker('ws:error');
MsgWorkerError.meta({
readyState: socket.readyState
@@ -75,11 +75,11 @@ let initSocket = (uri) => {
};
// send message to port(s) ============================================================================================
let sendToCurrentPort = (load) => {
let sendToCurrentPort = load => {
ports[ports.length - 1].postMessage(load);
};
let broadcastPorts = (load) => {
let broadcastPorts = load => {
// default: sent to all ports
let sentToPorts = ports;
@@ -114,7 +114,7 @@ let addPort = (port, characterId) => {
}
};
let getPortsByCharacterIds = (characterIds) => {
let getPortsByCharacterIds = characterIds => {
let ports = [];
for(let i = 0; i < characterPorts.length; i++){
@@ -128,8 +128,37 @@ let getPortsByCharacterIds = (characterIds) => {
return ports;
};
/**
*
* @param port
* @returns {int[]}
*/
let removePort = port => {
let characterIds = [];
// reverse loop required because of array index reset after splice()
let i = characterPorts.length;
while(i--){
if(characterPorts[i].port === port){
// collectt all character Ids mapped to the removed port
characterIds.push(characterPorts[i].characterId);
characterPorts.splice(i, 1);
}
}
let j = ports.length;
while(j--){
if(ports[j] === port){
ports.splice(j, 1);
}
}
// return unique characterIds
return [...new Set(characterIds)];
};
// "SharedWorker" connection ==========================================================================================
self.addEventListener('connect', (event) => { // jshint ignore:line
self.addEventListener('connect', event => { // jshint ignore:line
let port = event.ports[0];
addPort(port);
@@ -145,12 +174,18 @@ self.addEventListener('connect', (event) => { // jshint ignore:line
initSocket(data.uri);
break;
case 'ws:send':
let MsgSocket = {
socket.send(JSON.stringify({
task: MsgWorkerMessage.task(),
load: MsgWorkerMessage.data()
};
}));
break;
case 'sw:closePort':
port.close();
socket.send(JSON.stringify(MsgSocket));
socket.send(JSON.stringify({
task: MsgWorkerMessage.task(),
load: removePort(port)
}));
break;
case 'ws:close':
// closeSocket();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -203,9 +203,9 @@ define([], () => {
* @param version
*/
let showVersionInfo = (version) => {
console.ok('%c PATHFINDER',
'color: #477372; font-size: 25px; margin-left: 10px; line-height: 100px; text-shadow: 1px 1px 0 #212C30; ' +
'background: url(https://i.imgur.com/1Gw8mjL.png) no-repeat;');
console.ok('%c PATHFINDER',
'color: #477372; font-size: 25px; margin-left: 10px; line-height: 50px; text-shadow: 1px 1px 0 #212C30; ' +
'background: url(https://i.imgur.com/bhSr6LI.png) no-repeat;');
console.pf('Release: %s', version);
};

View File

@@ -4,13 +4,9 @@
define([
'app/util'
], function(Util){
], (Util) => {
'use strict';
let config = {
};
let sharedWorker = null;
let MsgWorker = null;
let characterId = null;
@@ -29,17 +25,13 @@ define([
* get SharedWorker Script path
* @returns {string}
*/
let getWorkerScript = () => {
return '/public/js/' + Util.getVersion() + '/app/worker/map.js';
};
let getWorkerScript = () => '/public/js/' + Util.getVersion() + '/app/worker/map.js';
/**
* get path to message object
* @returns {string}
*/
let getMessageWorkerObjectPath = () => {
return '/public/js/' + Util.getVersion() + '/app/worker/message.js';
};
let getMessageWorkerObjectPath = () => '/public/js/' + Util.getVersion() + '/app/worker/message.js';
/**
* init (connect) WebSocket within SharedWorker
@@ -51,14 +43,14 @@ define([
characterId: characterId,
});
sharedWorker.port.postMessage(MsgWorkerInit);
sendMessage(MsgWorkerInit);
};
/**
* init (start/connect) to "SharedWorker"
* init (start/connect) to "SharedWorker" thread
* -> set worker events
*/
let init = (config) => {
let init = config => {
// set characterId that is connected with this SharedWorker PORT
characterId = parseInt(config.characterId);
@@ -67,9 +59,9 @@ define([
MsgWorker = window.MsgWorker;
// start/connect to "SharedWorker"
sharedWorker = new SharedWorker( getWorkerScript(), getMessageWorkerObjectPath() );
sharedWorker = new SharedWorker(getWorkerScript(), getMessageWorkerObjectPath());
sharedWorker.port.addEventListener('message', (e) => {
sharedWorker.port.addEventListener('message', e => {
let MsgWorkerMessage = e.data;
Object.setPrototypeOf(MsgWorkerMessage, MsgWorker.prototype);
@@ -90,7 +82,7 @@ define([
}
}, false);
sharedWorker.onerror = (e) => {
sharedWorker.onerror = e => {
// could not connect to SharedWorker script -> send error back
let MsgWorkerError = new MsgWorker('sw:error');
MsgWorkerError.meta({
@@ -111,17 +103,49 @@ define([
});
};
/**
* send data to "SharedWorker" thread
* @param task
* @param data
*/
let send = (task, data) => {
let MsgWorkerSend = new MsgWorker('ws:send');
MsgWorkerSend.task(task);
MsgWorkerSend.data(data);
sharedWorker.port.postMessage(MsgWorkerSend);
sendMessage(MsgWorkerSend);
};
/**
* send close port task to "SharedWorker" thread
* -> this removes the port from its port collection and closes it
*/
let close = () => {
let MsgWorkerClose = new MsgWorker('sw:closePort');
MsgWorkerClose.task('unsubscribe');
sendMessage(MsgWorkerClose);
};
/**
*
* @param {window.MsgWorker} MsgWorkerSend
*/
let sendMessage = MsgWorkerSend => {
if(sharedWorker instanceof SharedWorker){
if(MsgWorkerSend instanceof window.MsgWorker){
sharedWorker.port.postMessage(MsgWorkerSend);
}else{
console.error('MsgWorkerSend must be instance of window.MsgWorker');
}
}else{
console.error('SharedWorker thread not found');
}
};
return {
getWebSocketURL: getWebSocketURL,
init: init,
send: send
send: send,
close: close
};
});

View File

@@ -221,7 +221,7 @@ define([
* @param payload
* @returns {Promise<any>}
*/
let initMapModule = (payload) => {
let initMapModule = payload => {
let initMapModuleExecutor = (resolve, reject) => {
// init browser tab change observer, Once the timers are available
@@ -248,10 +248,10 @@ define([
* @param payloadMapAccessData
* @returns {Promise<any>}
*/
let initMapWorker = (payloadMapAccessData) => {
let initMapWorker = payloadMapAccessData => {
let initMapWorkerExecutor = (resolve, reject) => {
let getPayload = (command) => {
let getPayload = command => {
return {
action: 'initMapWorker',
data: {
@@ -270,7 +270,7 @@ define([
// init SharedWorker for maps
MapWorker.init({
characterId: response.data.id,
characterId: response.data.id,
callbacks: {
onInit: (MsgWorkerMessage) => {
Util.setSyncStatus(MsgWorkerMessage.command);
@@ -552,6 +552,9 @@ define([
// Send map update request on tab close/reload, in order to save map changes that
// haven´t been saved through default update trigger
window.addEventListener('beforeunload', function(e){
// close connection to "SharedWorker"
MapWorker.close();
// save unsaved map changes ...
triggerMapUpdatePing();

View File

@@ -11,7 +11,9 @@ define([
'use strict';
let config = {
splashOverlayClass: 'pf-splash' // class for "splash" overlay
splashOverlayClass: 'pf-splash', // class for "splash" overlay
webSocketStatsId: 'pf-setup-webSocket-stats', // id for webSocket "stats" panel
webSocketRefreshStatsId: 'pf-setup-webSocket-stats-refresh' // class for "reload stats" button
};
/**
@@ -42,6 +44,18 @@ define([
});
};
/**
*
* @param container
* @param selector
*/
let setCollapseObserver = (container, selector) => {
container.find(selector).css({cursor: 'pointer'});
container.on('click', selector, function(){
$(this).find('.pf-animate-rotate').toggleClass('right');
});
};
/**
* set page observer
*/
@@ -52,9 +66,7 @@ define([
Util.initPageScroll(body);
// collapse ---------------------------------------------------------------------------------------------------
body.find('[data-toggle="collapse"]').css({cursor: 'pointer'}).on('click', function(){
$(this).find('.pf-animate-rotate').toggleClass('right');
});
setCollapseObserver(body, '[data-toggle="collapse"]');
// buttons ----------------------------------------------------------------------------------------------------
// exclude "download" && "navigation" buttons
@@ -128,6 +140,29 @@ define([
}
};
/**
* get WebSockets "subscriptions" <table> HTML
* @param subscriptionStats
* @returns {Promise<any>}
*/
let getWebSocketSubscriptionTable = subscriptionStats => {
let executor = resolve => {
requirejs(['text!templates/modules/subscriptions_table.html', 'mustache'], (template, Mustache) => {
let data = {
panelId: config.webSocketStatsId,
refreshButtonId: config.webSocketRefreshStatsId,
subStats: subscriptionStats,
channelCount: (Util.getObjVal(subscriptionStats, 'channels') || []).length
};
resolve(Mustache.render(template, data));
});
};
return new Promise(executor);
};
/**
* perform a basic check if Clients (browser) can connect to the webSocket server
*/
@@ -158,7 +193,7 @@ define([
let socketDangerCount = parseInt(badgeSocketDanger.text()) || 0;
if(data.uri){
let uriRow = webSocketPanel.find('.panel-body table tr');
let uriRow = webSocketPanel.find('.panel-body').filter(':first').find('table tr');
uriRow.find('td:nth-child(2) kbd').html(data.uri.value);
if(data.uri.status){
let statusIcon = uriRow.find('td:nth-child(3) i');
@@ -206,6 +241,18 @@ define([
}
});
/**
* @param socket
* @param task
* @param load
*/
let sendMessage = (socket, task, load) => {
socket.send(JSON.stringify({
task: task,
load: load
}));
};
// try to connect to WebSocket server
let socket = new WebSocket(webSocketURI);
@@ -219,10 +266,7 @@ define([
});
// sent token and check response
socket.send(JSON.stringify({
task: 'healthCheck',
load: tcpSocketPanel.data('token')
}));
sendMessage(socket, 'healthCheck', tcpSocketPanel.attr('data-token'));
webSocketPanel.hideLoadingAnimation();
};
@@ -230,7 +274,7 @@ define([
socket.onmessage = (e) => {
let response = JSON.parse(e.data);
if(response === 1){
if(Util.getObjVal(response, 'load.isValid') === true){
// SUCCESS
updateWebSocketPanel({
status: {
@@ -239,6 +283,23 @@ define([
class: 'txt-color-success'
}
});
// show subscription stats table
getWebSocketSubscriptionTable(Util.getObjVal(response, 'load.subStats')).then(payload => {
// remove existing table -> then insert new
$('#' + config.webSocketStatsId).remove();
$(payload).insertAfter(webSocketPanel).initTooltips();
let token = Util.getObjVal(response, 'load.token');
tcpSocketPanel.attr('data-token', token);
// set "reload stats" observer
$('#' + config.webSocketRefreshStatsId).on('click', function(){
$('#' + config.webSocketStatsId).showLoadingAnimation();
sendMessage(socket, 'healthCheck', token);
});
});
}else{
// Got response but INVALID
updateWebSocketPanel({
@@ -274,6 +335,8 @@ define([
});
webSocketPanel.hideLoadingAnimation();
$('#' + config.webSocketStatsId).remove();
};
};

View File

@@ -12,14 +12,14 @@ let ports = [];
let characterPorts = [];
// init "WebSocket" connection ========================================================================================
let initSocket = (uri) => {
let initSocket = uri => {
let MsgWorkerOpen = new MsgWorker('ws:open');
if(socket === null){
socket = new WebSocket(uri);
// "WebSocket" open -----------------------------------------------------------------------
socket.onopen = (e) => {
socket.onopen = e => {
MsgWorkerOpen.meta({
readyState: socket.readyState
});
@@ -28,7 +28,7 @@ let initSocket = (uri) => {
};
// "WebSocket message ---------------------------------------------------------------------
socket.onmessage = (e) => {
socket.onmessage = e => {
let response = JSON.parse(e.data);
let MsgWorkerSend = new MsgWorker('ws:send');
@@ -43,7 +43,7 @@ let initSocket = (uri) => {
};
// "WebSocket" close ----------------------------------------------------------------------
socket.onclose = (closeEvent) => {
socket.onclose = closeEvent => {
let MsgWorkerClosed = new MsgWorker('ws:closed');
MsgWorkerClosed.meta({
readyState: socket.readyState,
@@ -57,7 +57,7 @@ let initSocket = (uri) => {
};
// "WebSocket" error ----------------------------------------------------------------------
socket.onerror = (e) => {
socket.onerror = e => {
let MsgWorkerError = new MsgWorker('ws:error');
MsgWorkerError.meta({
readyState: socket.readyState
@@ -75,11 +75,11 @@ let initSocket = (uri) => {
};
// send message to port(s) ============================================================================================
let sendToCurrentPort = (load) => {
let sendToCurrentPort = load => {
ports[ports.length - 1].postMessage(load);
};
let broadcastPorts = (load) => {
let broadcastPorts = load => {
// default: sent to all ports
let sentToPorts = ports;
@@ -114,7 +114,7 @@ let addPort = (port, characterId) => {
}
};
let getPortsByCharacterIds = (characterIds) => {
let getPortsByCharacterIds = characterIds => {
let ports = [];
for(let i = 0; i < characterPorts.length; i++){
@@ -128,8 +128,37 @@ let getPortsByCharacterIds = (characterIds) => {
return ports;
};
/**
*
* @param port
* @returns {int[]}
*/
let removePort = port => {
let characterIds = [];
// reverse loop required because of array index reset after splice()
let i = characterPorts.length;
while(i--){
if(characterPorts[i].port === port){
// collectt all character Ids mapped to the removed port
characterIds.push(characterPorts[i].characterId);
characterPorts.splice(i, 1);
}
}
let j = ports.length;
while(j--){
if(ports[j] === port){
ports.splice(j, 1);
}
}
// return unique characterIds
return [...new Set(characterIds)];
};
// "SharedWorker" connection ==========================================================================================
self.addEventListener('connect', (event) => { // jshint ignore:line
self.addEventListener('connect', event => { // jshint ignore:line
let port = event.ports[0];
addPort(port);
@@ -145,12 +174,18 @@ self.addEventListener('connect', (event) => { // jshint ignore:line
initSocket(data.uri);
break;
case 'ws:send':
let MsgSocket = {
socket.send(JSON.stringify({
task: MsgWorkerMessage.task(),
load: MsgWorkerMessage.data()
};
}));
break;
case 'sw:closePort':
port.close();
socket.send(JSON.stringify(MsgSocket));
socket.send(JSON.stringify({
task: MsgWorkerMessage.task(),
load: removePort(port)
}));
break;
case 'ws:close':
// closeSocket();

View File

@@ -0,0 +1,102 @@
<div id="{{panelId}}" class="panel panel-default pricing-big">
<div class="panel-heading text-left">
<h3 class="panel-title">
<i class="fas fa-sitemap fa-rotate-270"></i>&nbsp;&nbsp;Web-Socket stats
<span class="pull-right">
<kbd title="current subscribed maps">{{channelCount}} maps</kbd>&nbsp;&nbsp;&nbsp;
<kbd title="current subscribed unique characters">{{ subStats.countSub }} chars</kbd>&nbsp;&nbsp;&nbsp;
<kbd title="current unique connections">{{ subStats.countCon }} con</kbd>
</span>
</h3>
</div>
{{#channelCount}}
<div class="panel-body no-padding text-left">
<table class="table">
<thead>
<tr>
<td class="col-xs-1"></td>
<td class="col-xs-1 text-right">id</td>
<td class="col-xs-2">map name</td>
<td class="col-xs-2"></td>
<td class="col-xs-2"></td>
<td class="col-xs-2"></td>
<td class="col-xs-2"></td>
</tr>
</thead>
<tbody>
{{#subStats.channels}}
<tr>
<td data-target=".subStats_channel_{{ channelId }}_col" data-toggle="collapse" style="cursor: pointer">
<i class="fas fa-fw fa-chevron-right pf-animate-rotate"></i>
</td>
<td class="text-right"><kbd>{{ channelId }}</kbd></td>
<td colspan="3">{{ channelName }}</td>
<td class="text-right" title="current subscribed characters"><kbd>{{ countSub }}&nbsp;chars</kbd></td>
<td class="text-right" title="current unique map connections"><kbd>{{ countCon }}&nbsp;con</kbd></td>
</tr>
<tr class="subStats_channel_{{ channelId }}_col collapse">
<td class="bg-color bg-color-tealDarkest">
<i class="fas fa-fw fa-level-up-alt fa-rotate-90"></i>
</td>
<td class="bg-color bg-color-tealDarkest"></td>
<td class="bg-color bg-color-tealDarkest"></td>
<td class="bg-color bg-color-tealDarkest text-right">id</td>
<td class="bg-color bg-color-tealDarkest" colspan="2">character name</td>
<td class="bg-color bg-color-tealDarkest"></td>
</tr>
{{#subscriptions}}
<tr class="subStats_channel_{{ channelId }}_col collapse">
<td class="text-right bg-color bg-color-tealDarker" data-target=".subStats_channel_{{ channelId }}_{{ characterId }}_col" data-toggle="collapse" style="cursor: pointer">
<i class="fas fa-fw fa-chevron-right pf-animate-rotate"></i>
</td>
<td class="bg-color bg-color-tealDarker"></td>
<td class="bg-color bg-color-tealDarker"></td>
<td class="bg-color bg-color-tealDarker text-right"><kbd>{{ characterId }} {{@index}}</kbd></td>
<td class="bg-color bg-color-tealDarker" colspan="2">{{ characterName }}</td>
<td class="bg-color bg-color-tealDarker text-right"><kbd>{{ countCon }}&nbsp;con</kbd></td>
</tr>
<tr class="subStats_channel_{{ channelId }}_col collapse">
<td class="no-padding" colspan="8">
<table class="table subStats_channel_{{ channelId }}_{{ characterId }}_col collapse">
<thead>
<tr>
<td class="col-xs-1 bg-color bg-color-tealDarkest text-right">
<i class="fas fa-level-up-alt fa-rotate-90"></i>
</td>
<td class="col-xs-1 bg-color bg-color-tealDarkest"></td>
<td class="col-xs-2 bg-color bg-color-tealDarkest">last update</td>
<td class="bg-color bg-color-tealDarkest text-right" colspan="3">client</td>
<td class="col-xs-2 bg-color bg-color-tealDarkest text-right">id</td>
</tr>
</thead>
<tbody>
{{#connections}}
<tr>
<td class="col-xs-1 bg-color bg-color-tealDark"></td>
<td class="col-xs-1 bg-color bg-color-tealDark"></td>
<td class="col-xs-1 bg-color bg-color-tealDark" title="{{ mTimeSendFormat1 }}" data-container="body"><kbd>{{ mTimeSendFormat2 }}</kbd></td>
<td class="bg-color bg-color-tealDark text-right" colspan="3"><kbd>{{ remoteAddress }}</kbd></td>
<td class="col-xs-2 bg-color bg-color-tealDark text-right"><kbd>#{{ resourceId }}</kbd></td>
</tr>
{{/connections}}
</tbody>
</table>
</td>
</tr>
{{/subscriptions}}
{{/subStats.channels}}
</tbody>
</table>
</div>
{{/channelCount}}
<div class="panel-footer btn-group btn-group-justified">
<span id="{{ refreshButtonId }}" class="btn btn-default" title="Reload WebSocket stats" data-container="body">
<i class="fas fa-fw fa-sync"></i> Reload stats
</span>
</div>
</div>

View File

@@ -509,7 +509,7 @@
<check if="{{ @dbInformation.info.dbConfig }}">
<tr>
<td data-target=".{{ @dbInformation.info.name }}_dbConfig_col" data-toggle="collapse" aria-expanded="false" aria-controls="{{ @dbInformation.info.name }}__dbConfig_col">
<td data-target=".{{ @dbInformation.info.name }}_dbConfig_col" data-toggle="collapse">
<i class="fas fa-fw fa-chevron-right pf-animate-rotate"></i>
</td>
<td class="col-sm-3 col-md-3 text-right">required</td>
@@ -1016,7 +1016,7 @@
<section id="pf-setup-socket">
<div class="container">
<h4><i class="fas fa-fw fa-exchange-alt"></i> Socket configuration <span class="txt-color txt-color-gray">[optional]</span></h4>
<h4><i class="fas fa-fw fa-random"></i> Socket configuration <span class="txt-color txt-color-gray">[optional]</span></h4>
<div class="row text-center">
@@ -1024,7 +1024,7 @@
<div class="col-xs-12 col-md-6 pf-landing-pricing-panel">
<div id="pf-setup-{{ @socketType }}" data-token="{{ @socketData.token }}" class="panel panel-default pricing-big" style="position: relative">
<div class="panel-heading text-left">
<h3 class="panel-title">{{ @socketData.label }}
<h3 class="panel-title"><i class="fas {{ @socketData.icon }}"></i>&nbsp;&nbsp;{{ @socketData.label }}
<check if="{{ @socketData.stats }}">
<span class="pull-right">
<kbd title="current active connections">{{ @socketData.stats.connections }} con</kbd>&nbsp;&nbsp;&nbsp;
@@ -1074,6 +1074,73 @@
</check>
</div>
{* show log data from "stats" array *}
<check if="{{ @socketData.stats.logs }}">
<div class="panel-body no-padding text-left">
<table class="table">
<thead>
<tr>
<td class="col-xs-1" data-target=".{{ @socketType }}_log_col" data-toggle="collapse">
<i class="fas fa-fw fa-chevron-right pf-animate-rotate"></i>
</td>
<td>client URI</td>
<td class="col-xs-2">file #line</td>
<td>message</td>
<td class="col-md-1 text-right">type</td>
</tr>
</thead>
<tbody>
<set logsCheck = 'OK' />
<repeat group="{{ @socketData.stats.logs }}" value="{{ @logData }}">
<set logTypeClass = "" />
<check if="{{ in_array('info', @logData.logTypes) }}">
<set logTypeClass = "txt-color-info" />
</check>
<check if="{{ in_array('error', @logData.logTypes) }}">
<set logTypeClass = "txt-color-warning" />
<set logsCheck = 'ERROR' />
</check>
<tr class="{{ @socketType }}_log_col {{ @logsCheck == 'OK' ? 'collapse' : '' }}">
<td class="bg-color bg-color-tealDarker" title="{{ @logData.mTimeFormat1 }}" data-container="body">
{{ @logData.mTimeFormat2 }}
</td>
<td class="bg-color bg-color-tealDarker" title="{{ @logData.remoteAddress }}" data-container="body">
<set cellValue = "{{ parse_url(@logData.remoteAddress, PHP_URL_HOST) . parse_url(@logData.remoteAddress, PHP_URL_PATH) }}" />
<check if="{{ parse_url(@logData.remoteAddress, PHP_URL_PORT) }}">
<set cellValue = "{{ @cellValue . ':' . parse_url(@logData.remoteAddress, PHP_URL_PORT) }}" />
</check>
<check if="{{ @logData.resourceId }}">
<set cellValue = "{{ @cellValue . '&nbsp;#' . @logData.resourceId }}" />
</check>
<check if="{{ @cellValue }}">
<true>
<kbd>{{ @cellValue}}</kbd>
</true>
<false>
<check if="{{ @logData.action != 'START' }}">
<kbd title="send to multiple clients" class="txt-color txt-color-success">broadcast</kbd>
</check>
</false>
</check>
</td>
<td class="bg-color bg-color-tealDarker" title="{{ @logData.fileName }} #{{ @logData.lineNumber }}" data-container="body">
<kbd>{{ mb_strimwidth(pathinfo(@logData.fileName, PATHINFO_FILENAME), 0, 15, '…') }}</kbd>
</td>
<td class="bg-color bg-color-tealDarker">{{ @logData.message }}</td>
<td class="text-right bg-color bg-color-tealDarker" title="{{ implode(@logData.logTypes, ', ') }}" data-placement="right" data-container="body">
<i class="fas fa-fw fa-tag txt-color {{ @logTypeClass }}"></i>
</td>
</tr>
</repeat>
</tbody>
</table>
</div>
</check>
</div>
</div>
</repeat>

View File

@@ -71,6 +71,7 @@
&.bg-color-grayDarker { background-color: $gray-darker !important; }
&.bg-color-magenta { background-color: $magenta !important; }
&.bg-color-tealLighter { background-color: $teal-lighter !important; }
&.bg-color-tealDark { background-color: $teal-dark !important; }
&.bg-color-tealDarker { background-color: $teal-darker !important; }
&.bg-color-tealDarkest { background-color: $teal-darkest !important; }
&.bg-color-redLight { background-color: $redLight !important; }

View File

@@ -711,9 +711,7 @@
// setup page -----------------------------------------------------------------
.pf-body[data-script='setup']{
body{
user-select: text;
}
user-select: text;
.navbar-brand:hover{
color: #777; // overwrite default
@@ -737,6 +735,10 @@
}
}
}
.panel-heading{
padding-right: 8px; // overwrite default
}
}
// TEST ---

View File

@@ -545,6 +545,16 @@ table{
table-layout: fixed;
}
&.collapsing{
@include transition( height .01s ease );
}
&.collapse.in{
display: table;
}
tr{
&.collapsing{
@include transition( height .01s ease );