diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php index 281cb325..26692c7e 100644 --- a/app/main/controller/accesscontroller.php +++ b/app/main/controller/accesscontroller.php @@ -74,15 +74,25 @@ class AccessController extends Controller { return $loginStatus; } + /** + * broadcast MapModel to clients + * @see broadcastMapData() + * @param Pathfinder\MapModel $map + */ + protected function broadcastMap(Pathfinder\MapModel $map) : void { + $this->broadcastMapData($this->getFormattedMapData($map)); + } + + /** * broadcast map data to clients * -> send over TCP Socket - * @param Pathfinder\MapModel $map - * @throws \Exception + * @param array|null $mapData */ - protected function broadcastMapData(Pathfinder\MapModel $map) : void { - $mapData = $this->getFormattedMapData($map); - $this->getF3()->webSocket()->write('mapUpdate', $mapData); + protected function broadcastMapData(?array $mapData) : void { + if(!empty($mapData)){ + $this->getF3()->webSocket()->write('mapUpdate', $mapData); + } } /** @@ -91,16 +101,27 @@ class AccessController extends Controller { * @return array * @throws \Exception */ - protected function getFormattedMapData(Pathfinder\MapModel $map) : array { - $mapData = $map->getData(); - return [ - 'config' => $mapData->mapData, - 'data' => [ - 'systems' => $mapData->systems, - 'connections' => $mapData->connections, - ] - ]; + /** + * @param Pathfinder\MapModel $map + * @return array|null + */ + protected function getFormattedMapData(Pathfinder\MapModel $map) : ?array { + $data = null; + try{ + $mapData = $map->getData(); + $data = [ + 'config' => $mapData->mapData, + 'data' => [ + 'systems' => $mapData->systems, + 'connections' => $mapData->connections, + ] + ]; + }catch(\Exception $e){ + + } + + return $data; } } \ No newline at end of file diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index 667ee575..516eb381 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -660,7 +660,7 @@ class Map extends Controller\AccessController { $this->getF3()->webSocket()->write('mapAccess', $mapAccess); // map has (probably) active connections that should receive map Data - $this->broadcastMapData($map); + $this->broadcastMap($map); } /** @@ -689,9 +689,12 @@ class Map extends Controller\AccessController { unset($characterData->corporation->rights); } + // access token + $token = bin2hex(random_bytes(16)); + $return->data = [ 'id' => $activeCharacter->_id, - 'token' => bin2hex(random_bytes(16)), // token for character access + 'token' => $token, // character access 'characterData' => $characterData, 'mapData' => [] ]; @@ -700,7 +703,7 @@ class Map extends Controller\AccessController { foreach($maps as $map){ $return->data['mapData'][] = [ 'id' => $map->_id, - 'token' => bin2hex(random_bytes(16)), // token for map access + 'token' => $token, // map access 'name' => $map->name ]; } @@ -722,41 +725,33 @@ class Map extends Controller\AccessController { } /** - * update map data - * -> function is called continuously (trigger) by any active client - * @param \Base $f3 - * @throws Exception + * update maps with $mapsData where $character has access to + * @param Pathfinder\CharacterModel $character + * @param array $mapsData + * @return \stdClass */ - public function updateData(\Base $f3){ - $postData = (array)$f3->get('POST'); - $mapData = (array)$postData['mapData']; - $userDataRequired = (bool)$postData['getUserData']; - + protected function updateMapsData(Pathfinder\CharacterModel $character, array $mapsData) : \stdClass { $return = (object) []; $return->error = []; + $return->mapData = []; - $activeCharacter = $this->getCharacter(); + $mapIdsChanged = []; + $maps = $character->getMaps(); - // get current map data - $maps = $activeCharacter->getMaps(); - - // if there is any system/connection change data submitted -> save new data - if( !empty($maps) && !empty($mapData) ){ - - // loop all submitted map data that should be saved + if(!empty($mapsData) && !empty($maps)){ + // loop all $mapsData that should be saved // -> currently there will only be ONE map data change submitted -> single loop - foreach($mapData as $data){ - + foreach($mapsData as $data){ $systems = []; $connections = []; // check whether system data and/or connection data is send // empty arrays are not included in ajax requests - if( isset($data['data']['systems']) ){ + if(isset($data['data']['systems'])){ $systems = (array)$data['data']['systems']; } - if( isset($data['data']['connections']) ){ + if(isset($data['data']['connections'])){ $connections = (array)$data['data']['connections']; } @@ -769,15 +764,15 @@ class Map extends Controller\AccessController { // loop current user maps and check for changes foreach($maps as $map){ - $mapChanged = false; - // update system data ------------------------------------------------------------------------- foreach($systems as $i => $systemData){ // check if current system belongs to the current map if($system = $map->getSystemById((int)$systemData['id'])){ $system->copyfrom($systemData, ['alias', 'status', 'position', 'locked', 'rallyUpdated', 'rallyPoke']); - if($system->save($activeCharacter)){ - $mapChanged = true; + if($system->save($character)){ + if(!in_array($map->_id, $mapIdsChanged)){ + $mapIdsChanged[] = $map->_id; + } // one system belongs to ONE map -> speed up for multiple maps unset($systemData[$i]); }else{ @@ -791,8 +786,10 @@ class Map extends Controller\AccessController { // check if the current connection belongs to the current map if($connection = $map->getConnectionById((int)$connectionData['id'])){ $connection->copyfrom($connectionData, ['scope', 'type', 'endpoints']); - if($connection->save($activeCharacter)){ - $mapChanged = true; + if($connection->save($character)){ + if(!in_array($map->_id, $mapIdsChanged)){ + $mapIdsChanged[] = $map->_id; + } // one connection belongs to ONE map -> speed up for multiple maps unset($connectionData[$i]); }else{ @@ -800,17 +797,39 @@ class Map extends Controller\AccessController { } } } - - if($mapChanged){ - $this->broadcastMapData($map); - } } } } } - // format map Data for return - $return->mapData = $this->getFormattedMapsData($maps); + foreach($maps as $map){ + // format map Data for return/broadcast + if($mapData = $this->getFormattedMapData($map)){ + if(in_array($map->_id, $mapIdsChanged)){ + $this->broadcastMapData($mapData); + } + + $return->mapData[] = $mapData; + } + } + + return $return; + } + + /** + * update map data + * -> function is called continuously (trigger) by any active client + * @param \Base $f3 + * @throws Exception + */ + public function updateData(\Base $f3){ + $postData = (array)$f3->get('POST'); + $mapsData = (array)$postData['mapData']; + $userDataRequired = (bool)$postData['getUserData']; + + $activeCharacter = $this->getCharacter(); + + $return = $this->updateMapsData($activeCharacter, $mapsData); // if userData is requested -> add it as well // -> Only first trigger call should request this data! @@ -822,18 +841,22 @@ class Map extends Controller\AccessController { } /** - * get formatted map data - * @param Pathfinder\MapModel[] $mapModels - * @return array + * onUnload map sync + * @see https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon + * @param \Base $f3 * @throws Exception */ - protected function getFormattedMapsData(array $mapModels) : array { - $mapData = []; - foreach($mapModels as $mapModel){ - $mapData[] = $this->getFormattedMapData($mapModel); - } + public function updateUnloadData(\Base $f3){ + $postData = (array)$f3->get('POST'); - return $mapData; + if(!empty($mapsData = (string)$postData['mapData'])){ + $mapsData = (array)json_decode($mapsData, true); + if(($jsonError = json_last_error()) === JSON_ERROR_NONE){ + $activeCharacter = $this->getCharacter(); + + $this->updateMapsData($activeCharacter, $mapsData); + } + } } /** @@ -1137,7 +1160,7 @@ class Map extends Controller\AccessController { } if($mapDataChanged){ - $this->broadcastMapData($map); + $this->broadcastMap($map); } return $map; diff --git a/app/main/controller/api/rest/connection.php b/app/main/controller/api/rest/connection.php index 5247da4d..9bcc1b28 100644 --- a/app/main/controller/api/rest/connection.php +++ b/app/main/controller/api/rest/connection.php @@ -56,7 +56,7 @@ class Connection extends AbstractRestController { $connectionData = $connection->getData(); // broadcast map changes - $this->broadcastMapData($connection->mapId); + $this->broadcastMap($connection->mapId); } } } @@ -95,7 +95,7 @@ class Connection extends AbstractRestController { // broadcast map changes if(count($deletedConnectionIds)){ - $this->broadcastMapData($map); + $this->broadcastMap($map); } } } diff --git a/app/main/controller/api/rest/system.php b/app/main/controller/api/rest/system.php index 94b5fe3e..bed3422e 100644 --- a/app/main/controller/api/rest/system.php +++ b/app/main/controller/api/rest/system.php @@ -144,7 +144,7 @@ class System extends AbstractRestController { } // broadcast map changes if(count($deletedSystemIds)){ - $this->broadcastMapData($map); + $this->broadcastMap($map); } } } @@ -187,7 +187,7 @@ class System extends AbstractRestController { $newSystem->clearCacheData(); // broadcast map changes - $this->broadcastMapData($newSystem->mapId); + $this->broadcastMap($newSystem->mapId); return $newSystem; } diff --git a/app/main/cron/characterupdate.php b/app/main/cron/characterupdate.php index 325b1339..b1cb2055 100644 --- a/app/main/cron/characterupdate.php +++ b/app/main/cron/characterupdate.php @@ -64,10 +64,15 @@ class CharacterUpdate extends AbstractCron { * @var $characterLog Pathfinder\CharacterLogModel */ if(is_object($characterLog->characterId)){ - // force characterLog as "updated" even if no changes were made - $characterLog->characterId->updateLog([ - 'markUpdated' => true - ]); + if($accessToken = $characterLog->characterId->getAccessToken()){ + if($this->isOnline($accessToken)){ + // force characterLog as "updated" even if no changes were made + $characterLog->touch('updated'); + $characterLog->save(); + }else{ + $characterLog->erase(); + } + } }else{ // character_log does not have a character assigned -> delete $characterLog->erase(); diff --git a/app/main/model/pathfinder/charactermodel.php b/app/main/model/pathfinder/charactermodel.php index 8fd7d099..eb5e4ac5 100644 --- a/app/main/model/pathfinder/charactermodel.php +++ b/app/main/model/pathfinder/charactermodel.php @@ -507,7 +507,7 @@ class CharacterModel extends AbstractPathfinderModel { /** * get ESI API "access_token" from OAuth - * @return bool|mixed + * @return bool|string */ public function getAccessToken(){ $accessToken = false; @@ -797,6 +797,31 @@ class CharacterModel extends AbstractPathfinderModel { $this->roleId = $this->requestRole(); } + /** + * get online status data from ESI + * @param string $accessToken + * @return array + */ + protected function getOnlineData(string $accessToken) : array { + return self::getF3()->ccpClient()->getCharacterOnlineData($this->_id, $accessToken); + } + + /** + * check online state from ESI + * @param string $accessToken + * @return bool + */ + public function isOnline(string $accessToken) : bool { + $isOnline = false; + $onlineData = $this->getOnlineData($accessToken); + + if($onlineData['online'] === true){ + $isOnline = true; + } + + return $isOnline; + } + /** * update character log (active system, ...) * -> API request for character log data @@ -815,170 +840,162 @@ class CharacterModel extends AbstractPathfinderModel { $this->hasBasicScopes() ){ // Try to pull data from API - if( $accessToken = $this->getAccessToken() ){ - $onlineData = self::getF3()->ccpClient()->getCharacterOnlineData($this->_id, $accessToken); + if($accessToken = $this->getAccessToken()){ + if($this->isOnline($accessToken)){ + $locationData = self::getF3()->ccpClient()->getCharacterLocationData($this->_id, $accessToken); - // check whether character is currently ingame online - if(is_bool($onlineData['online'])){ - if($onlineData['online'] === true){ - $locationData = self::getF3()->ccpClient()->getCharacterLocationData($this->_id, $accessToken); + if( !empty($locationData['system']['id']) ){ + // character is currently in-game - if( !empty($locationData['system']['id']) ){ - // character is currently in-game + // get current $characterLog or get new --------------------------------------------------- + if( !($characterLog = $this->getLog()) ){ + // create new log + $characterLog = $this->rel('characterLog'); + } - // get current $characterLog or get new --------------------------------------------------- - if( !($characterLog = $this->getLog()) ){ - // create new log - $characterLog = $this->rel('characterLog'); - } + // get current log data and modify on change + $logData = json_decode(json_encode( $characterLog->getData()), true); - // get current log data and modify on change - $logData = json_decode(json_encode( $characterLog->getData()), true); + // check system and station data for changes ---------------------------------------------- - // check system and station data for changes ---------------------------------------------- + // IDs for "systemId", "stationId" that require more data + $lookupUniverseIds = []; - // IDs for "systemId", "stationId" that require more data - $lookupUniverseIds = []; + if( + empty($logData['system']['name']) || + $logData['system']['id'] !== $locationData['system']['id'] + ){ + // system changed -> request "system name" for current system + $lookupUniverseIds[] = $locationData['system']['id']; + } + if( !empty($locationData['station']['id']) ){ if( - empty($logData['system']['name']) || - $logData['system']['id'] !== $locationData['system']['id'] + empty($logData['station']['name']) || + $logData['station']['id'] !== $locationData['station']['id'] ){ - // system changed -> request "system name" for current system - $lookupUniverseIds[] = $locationData['system']['id']; + // station changed -> request "station name" for current station + $lookupUniverseIds[] = $locationData['station']['id']; } + }else{ + unset($logData['station']); + } - if( !empty($locationData['station']['id']) ){ + $logData = array_replace_recursive($logData, $locationData); + + // get "more" data for systemId and/or stationId ----------------------------------------- + if( !empty($lookupUniverseIds) ){ + // get "more" information for some Ids (e.g. name) + $universeData = self::getF3()->ccpClient()->getUniverseNamesData($lookupUniverseIds); + + if( !empty($universeData) && !isset($universeData['error']) ){ + // We expect max ONE system AND/OR station data, not an array of e.g. systems + if(!empty($universeData['system'])){ + $universeData['system'] = reset($universeData['system']); + } + if(!empty($universeData['station'])){ + $universeData['station'] = reset($universeData['station']); + } + + $logData = array_replace_recursive($logData, $universeData); + }else{ + // this is important! universe data is a MUST HAVE! + $deleteLog = true; + } + } + + // check structure data for changes ------------------------------------------------------- + if(!$deleteLog){ + + // IDs for "structureId" that require more data + $lookupStructureId = 0; + if( !empty($locationData['structure']['id']) ){ if( - empty($logData['station']['name']) || - $logData['station']['id'] !== $locationData['station']['id'] + empty($logData['structure']['name']) || + $logData['structure']['id'] !== $locationData['structure']['id'] ){ - // station changed -> request "station name" for current station - $lookupUniverseIds[] = $locationData['station']['id']; + // structure changed -> request "structure name" for current station + $lookupStructureId = $locationData['structure']['id']; } }else{ - unset($logData['station']); + unset($logData['structure']); } - $logData = array_replace_recursive($logData, $locationData); - - // get "more" data for systemId and/or stationId ----------------------------------------- - if( !empty($lookupUniverseIds) ){ - // get "more" information for some Ids (e.g. name) - $universeData = self::getF3()->ccpClient()->getUniverseNamesData($lookupUniverseIds); - - if( !empty($universeData) && !isset($universeData['error']) ){ - // We expect max ONE system AND/OR station data, not an array of e.g. systems - if(!empty($universeData['system'])){ - $universeData['system'] = reset($universeData['system']); - } - if(!empty($universeData['station'])){ - $universeData['station'] = reset($universeData['station']); - } - - $logData = array_replace_recursive($logData, $universeData); - }else{ - // this is important! universe data is a MUST HAVE! - $deleteLog = true; - } - } - - // check structure data for changes ------------------------------------------------------- - if(!$deleteLog){ - - // IDs for "structureId" that require more data - $lookupStructureId = 0; - if( !empty($locationData['structure']['id']) ){ - if( - empty($logData['structure']['name']) || - $logData['structure']['id'] !== $locationData['structure']['id'] - ){ - // structure changed -> request "structure name" for current station - $lookupStructureId = $locationData['structure']['id']; - } + // get "more" data for structureId --------------------------------------------------- + if($lookupStructureId > 0){ + /** + * @var $structureModel Universe\StructureModel + */ + $structureModel = Universe\AbstractUniverseModel::getNew('StructureModel'); + $structureModel->loadById($lookupStructureId, $accessToken, $additionalOptions); + if(!$structureModel->dry()){ + $structureData['structure'] = (array)$structureModel->getData(); + $logData = array_replace_recursive($logData, $structureData); }else{ unset($logData['structure']); } + } + } - // get "more" data for structureId --------------------------------------------------- - if($lookupStructureId > 0){ - /** - * @var $structureModel Universe\StructureModel - */ - $structureModel = Universe\AbstractUniverseModel::getNew('StructureModel'); - $structureModel->loadById($lookupStructureId, $accessToken, $additionalOptions); - if(!$structureModel->dry()){ - $structureData['structure'] = (array)$structureModel->getData(); - $logData = array_replace_recursive($logData, $structureData); - }else{ - unset($logData['structure']); - } + // check ship data for changes ------------------------------------------------------------ + if( !$deleteLog ){ + $shipData = self::getF3()->ccpClient()->getCharacterShipData($this->_id, $accessToken); + + // IDs for "shipTypeId" that require more data + $lookupShipTypeId = 0; + if( !empty($shipData['ship']['typeId']) ){ + if( + empty($logData['ship']['typeName']) || + $logData['ship']['typeId'] !== $shipData['ship']['typeId'] + ){ + // ship changed -> request "station name" for current station + $lookupShipTypeId = $shipData['ship']['typeId']; } + + // "shipName"/"shipId" could have changed... + $logData = array_replace_recursive($logData, $shipData); + }else{ + // ship data should never be empty -> keep current one + //unset($logData['ship']); + $invalidResponse = true; } - // check ship data for changes ------------------------------------------------------------ - if( !$deleteLog ){ - $shipData = self::getF3()->ccpClient()->getCharacterShipData($this->_id, $accessToken); - - // IDs for "shipTypeId" that require more data - $lookupShipTypeId = 0; - if( !empty($shipData['ship']['typeId']) ){ - if( - empty($logData['ship']['typeName']) || - $logData['ship']['typeId'] !== $shipData['ship']['typeId'] - ){ - // ship changed -> request "station name" for current station - $lookupShipTypeId = $shipData['ship']['typeId']; - } - - // "shipName"/"shipId" could have changed... + // get "more" data for shipTypeId ---------------------------------------------------- + if($lookupShipTypeId > 0){ + /** + * @var $typeModel Universe\TypeModel + */ + $typeModel = Universe\AbstractUniverseModel::getNew('TypeModel'); + $typeModel->loadById($lookupShipTypeId, '', $additionalOptions); + if(!$typeModel->dry()){ + $shipData['ship'] = (array)$typeModel->getShipData(); $logData = array_replace_recursive($logData, $shipData); }else{ - // ship data should never be empty -> keep current one - //unset($logData['ship']); - $invalidResponse = true; - } - - // get "more" data for shipTypeId ---------------------------------------------------- - if($lookupShipTypeId > 0){ - /** - * @var $typeModel Universe\TypeModel - */ - $typeModel = Universe\AbstractUniverseModel::getNew('TypeModel'); - $typeModel->loadById($lookupShipTypeId, '', $additionalOptions); - if(!$typeModel->dry()){ - $shipData['ship'] = (array)$typeModel->getShipData(); - $logData = array_replace_recursive($logData, $shipData); - }else{ - // this is important! ship data is a MUST HAVE! - $deleteLog = true; - } + // this is important! ship data is a MUST HAVE! + $deleteLog = true; } } + } - if( !$deleteLog ){ - // mark log as "updated" even if no changes were made - if($additionalOptions['markUpdated'] === true){ - $characterLog->touch('updated'); - } - - $characterLog->setData($logData); - $characterLog->characterId = $this->id; - $characterLog->save(); - - $this->characterLog = $characterLog; + if( !$deleteLog ){ + // mark log as "updated" even if no changes were made + if($additionalOptions['markUpdated'] === true){ + $characterLog->touch('updated'); } - }else{ - // systemId should always exists - $invalidResponse = true; + + $characterLog->setData($logData); + $characterLog->characterId = $this->id; + $characterLog->save(); + + $this->characterLog = $characterLog; } }else{ - // user is in-game offline - $deleteLog = true; + // systemId should always exists + $invalidResponse = true; } }else{ - // online status request failed - $invalidResponse = true; + // user is in-game offline + $deleteLog = true; } }else{ // access token request failed diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 1953ca21..aafe85a8 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -287,7 +287,8 @@ EXECUTION_LIMIT = 50 ; CACHE =========================================================================================== [PATHFINDER.CACHE] -; Delete character log data if nothing (ship/system/...) changed for X seconds +; Checks "character log" data by cronjob after x seconds +; If character is ingame offline -> delete "character log" ; Syntax: Integer (seconds) ; Default: 180 CHARACTER_LOG_INACTIVE = 180 diff --git a/app/routes.ini b/app/routes.ini index fe164ec8..d411c0ec 100644 --- a/app/routes.ini +++ b/app/routes.ini @@ -18,6 +18,9 @@ GET|POST /api/@controller/@action [ajax] = Controller\Api\@cont GET|POST /api/@controller/@action/@arg1 [ajax] = Controller\Api\@controller->@action, 0, 512 GET|POST /api/@controller/@action/@arg1/@arg2 [ajax] = Controller\Api\@controller->@action, 0, 512 +; onUnload route or final map sync (@see https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon) +POST /api/map/updateUnloadData = Controller\Api\map->updateUnloadData, 0, 512 + [maps] ; REST API wildcard endpoints (not cached, throttled) /api/rest/@controller* [ajax] = Controller\Api\Rest\@controller, 0, 512 diff --git a/js/app/init.js b/js/app/init.js index 880e7bdc..53b0fa2d 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -27,6 +27,7 @@ define(['jquery'], ($) => { getAccessData: '/api/map/getAccessData', // ajax URL - get map access tokens (WebSocket) updateMapData: '/api/map/updateData', // ajax URL - main map update trigger updateUserData: '/api/map/updateUserData', // ajax URL - main map user data trigger + updateUnloadData: '/api/map/updateUnloadData', // post URL - for my sync onUnload // map API saveMap: '/api/map/save', // ajax URL - save/update map deleteMap: '/api/map/delete', // ajax URL - delete map diff --git a/js/app/map/worker.js b/js/app/map/worker.js index b922aa51..b65d910d 100644 --- a/js/app/map/worker.js +++ b/js/app/map/worker.js @@ -121,9 +121,12 @@ define([ * -> this removes the port from its port collection and closes it */ let close = () => { - let MsgWorkerClose = new MsgWorker('sw:closePort'); - MsgWorkerClose.task('unsubscribe'); - sendMessage(MsgWorkerClose); + // check if MsgWorker is available (SharedWorker was initialized) + if(MsgWorker){ + let MsgWorkerClose = new MsgWorker('sw:closePort'); + MsgWorkerClose.task('unsubscribe'); + sendMessage(MsgWorkerClose); + } }; /** diff --git a/js/app/mappage.js b/js/app/mappage.js index 5a9a701a..2f81d548 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -549,14 +549,42 @@ define([ // initial start of the map update function triggerMapUpdatePing(true); + /** + * handles "final" map update request before window.unload event + * -> navigator.sendBeacon browser support required + * ajax would not work here, because browsers might cancel the request! + * @param mapModule + */ + let mapUpdateUnload = mapModule => { + // get updated map data + let mapData = ModuleMap.getMapModuleDataForUpdate(mapModule); + + if(mapData.length){ + let fd = new FormData(); + fd.set('mapData', JSON.stringify(mapData)); + navigator.sendBeacon(Init.path.updateUnloadData, fd); + + console.info('Map update request send by: %O', navigator.sendBeacon); + } + }; + // 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" + // close "SharedWorker" connection MapWorker.close(); + // clear periodic update timeouts + // -> this function will handle the final map update request + clearUpdateTimeouts(); + // save unsaved map changes ... - triggerMapUpdatePing(); + if(navigator.sendBeacon){ + mapUpdateUnload(mapModule); + }else{ + // fallback if sendBeacon() is not supported by browser + triggerMapUpdatePing(); + } // check if character should be switched on reload or current character should be loaded afterwards let characterSwitch = Boolean( $('body').data('characterSwitch') ); @@ -569,7 +597,7 @@ define([ // IMPORTANT, return false in order to not "abort" ajax request in background! return false; - }); + }, false); }; diff --git a/js/app/module_map.js b/js/app/module_map.js index 10b7f14a..eab6ee8c 100644 --- a/js/app/module_map.js +++ b/js/app/module_map.js @@ -1339,16 +1339,17 @@ define([ * collect all data (systems/connections) for export/save from each active map in the map module * if no change detected -> do not attach map data to return array * @param mapModule + * @param filter * @returns {Array} */ - let getMapModuleDataForUpdate = mapModule => { + let getMapModuleDataForUpdate = (mapModule, filter = ['hasId', 'hasChanged']) => { // get all active map elements for module let mapElements = getMaps(mapModule); let data = []; for(let i = 0; i < mapElements.length; i++){ // get all changed (system / connection) data from this map - let mapData = Map.getMapDataForSync($(mapElements[i]), ['hasId', 'hasChanged']); + let mapData = Map.getMapDataForSync($(mapElements[i]), filter); if(mapData !== false){ if( mapData.data.systems.length > 0 || diff --git a/js/app/worker/map.js b/js/app/worker/map.js index 26e737d0..53975bd6 100644 --- a/js/app/worker/map.js +++ b/js/app/worker/map.js @@ -182,10 +182,20 @@ self.addEventListener('connect', event => { // jshint ignore:line case 'sw:closePort': port.close(); - socket.send(JSON.stringify({ - task: MsgWorkerMessage.task(), - load: removePort(port) - })); + // remove port from store + // -> charIds managed by closed port + let characterIds = removePort(port); + + // check if there are still other ports active that manage removed ports + // .. if not -> send "unsubscribe" event to WebSocket server + let portsLeft = getPortsByCharacterIds(characterIds); + + if(!portsLeft.length){ + socket.send(JSON.stringify({ + task: MsgWorkerMessage.task(), + load: characterIds + })); + } break; case 'ws:close': // closeSocket(); diff --git a/public/js/v1.5.2/app/init.js b/public/js/v1.5.2/app/init.js index 880e7bdc..53b0fa2d 100644 --- a/public/js/v1.5.2/app/init.js +++ b/public/js/v1.5.2/app/init.js @@ -27,6 +27,7 @@ define(['jquery'], ($) => { getAccessData: '/api/map/getAccessData', // ajax URL - get map access tokens (WebSocket) updateMapData: '/api/map/updateData', // ajax URL - main map update trigger updateUserData: '/api/map/updateUserData', // ajax URL - main map user data trigger + updateUnloadData: '/api/map/updateUnloadData', // post URL - for my sync onUnload // map API saveMap: '/api/map/save', // ajax URL - save/update map deleteMap: '/api/map/delete', // ajax URL - delete map diff --git a/public/js/v1.5.2/app/map/worker.js b/public/js/v1.5.2/app/map/worker.js index b922aa51..b65d910d 100644 --- a/public/js/v1.5.2/app/map/worker.js +++ b/public/js/v1.5.2/app/map/worker.js @@ -121,9 +121,12 @@ define([ * -> this removes the port from its port collection and closes it */ let close = () => { - let MsgWorkerClose = new MsgWorker('sw:closePort'); - MsgWorkerClose.task('unsubscribe'); - sendMessage(MsgWorkerClose); + // check if MsgWorker is available (SharedWorker was initialized) + if(MsgWorker){ + let MsgWorkerClose = new MsgWorker('sw:closePort'); + MsgWorkerClose.task('unsubscribe'); + sendMessage(MsgWorkerClose); + } }; /** diff --git a/public/js/v1.5.2/app/mappage.js b/public/js/v1.5.2/app/mappage.js index 5a9a701a..2f81d548 100644 --- a/public/js/v1.5.2/app/mappage.js +++ b/public/js/v1.5.2/app/mappage.js @@ -549,14 +549,42 @@ define([ // initial start of the map update function triggerMapUpdatePing(true); + /** + * handles "final" map update request before window.unload event + * -> navigator.sendBeacon browser support required + * ajax would not work here, because browsers might cancel the request! + * @param mapModule + */ + let mapUpdateUnload = mapModule => { + // get updated map data + let mapData = ModuleMap.getMapModuleDataForUpdate(mapModule); + + if(mapData.length){ + let fd = new FormData(); + fd.set('mapData', JSON.stringify(mapData)); + navigator.sendBeacon(Init.path.updateUnloadData, fd); + + console.info('Map update request send by: %O', navigator.sendBeacon); + } + }; + // 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" + // close "SharedWorker" connection MapWorker.close(); + // clear periodic update timeouts + // -> this function will handle the final map update request + clearUpdateTimeouts(); + // save unsaved map changes ... - triggerMapUpdatePing(); + if(navigator.sendBeacon){ + mapUpdateUnload(mapModule); + }else{ + // fallback if sendBeacon() is not supported by browser + triggerMapUpdatePing(); + } // check if character should be switched on reload or current character should be loaded afterwards let characterSwitch = Boolean( $('body').data('characterSwitch') ); @@ -569,7 +597,7 @@ define([ // IMPORTANT, return false in order to not "abort" ajax request in background! return false; - }); + }, false); }; diff --git a/public/js/v1.5.2/app/module_map.js b/public/js/v1.5.2/app/module_map.js index 10b7f14a..eab6ee8c 100644 --- a/public/js/v1.5.2/app/module_map.js +++ b/public/js/v1.5.2/app/module_map.js @@ -1339,16 +1339,17 @@ define([ * collect all data (systems/connections) for export/save from each active map in the map module * if no change detected -> do not attach map data to return array * @param mapModule + * @param filter * @returns {Array} */ - let getMapModuleDataForUpdate = mapModule => { + let getMapModuleDataForUpdate = (mapModule, filter = ['hasId', 'hasChanged']) => { // get all active map elements for module let mapElements = getMaps(mapModule); let data = []; for(let i = 0; i < mapElements.length; i++){ // get all changed (system / connection) data from this map - let mapData = Map.getMapDataForSync($(mapElements[i]), ['hasId', 'hasChanged']); + let mapData = Map.getMapDataForSync($(mapElements[i]), filter); if(mapData !== false){ if( mapData.data.systems.length > 0 || diff --git a/public/js/v1.5.2/app/worker/map.js b/public/js/v1.5.2/app/worker/map.js index 26e737d0..53975bd6 100644 --- a/public/js/v1.5.2/app/worker/map.js +++ b/public/js/v1.5.2/app/worker/map.js @@ -182,10 +182,20 @@ self.addEventListener('connect', event => { // jshint ignore:line case 'sw:closePort': port.close(); - socket.send(JSON.stringify({ - task: MsgWorkerMessage.task(), - load: removePort(port) - })); + // remove port from store + // -> charIds managed by closed port + let characterIds = removePort(port); + + // check if there are still other ports active that manage removed ports + // .. if not -> send "unsubscribe" event to WebSocket server + let portsLeft = getPortsByCharacterIds(characterIds); + + if(!portsLeft.length){ + socket.send(JSON.stringify({ + task: MsgWorkerMessage.task(), + load: characterIds + })); + } break; case 'ws:close': // closeSocket();