import TabCache from './TabCache'; import Tab from './Tab'; import EventEmitter from 'eventemitter3'; const EVENT_NAMES = { 'page': 'onPage' } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } export default class TabHandler { /** * [constructor * - create instance of TabCache * ] * @param {Number} projectId * @param {Extension} extension [instance of Extension-Class] */ constructor(projectId, extension) { this.extension = extension; this.tab2precursor_id = {}; this.closeTab = this.closeTab.bind(this); this._onFocus = this._onFocus.bind(this); this._updateDuration = this._updateDuration.bind(this); this.projectId = projectId; this.tabCache = new TabCache(projectId, 0); this.UPDATE_INTERVAL_DURATION = 1000; this.event = new EventEmitter(); this.onFocusTabInterval = null; this.openerTabId2tab = {}; this.tabID2Opener = {}; this.debug = false; } /** * [_getHashCode return hashcode from string] * @param {String} str [default: ''] * @return {String} */ _getHashCode(str=''){ var hash = 5381, i = str.length while(i) hash = (hash * 33) ^ str.charCodeAt(--i) return hash >>> 0; } getTabOpenerID(tabId){ let openerid = this.tabID2Opener[tabId]; //id = this.tab2precursor_id.hasOwnProperty(openerid)? this.tab2precursor_id[openerid]: null; if (this.tabs.hasOwnProperty(openerid)){ return this.tabs[openerid].id; } else { return 'TabHandler.js:getTabOpenerID:03)'; } } /** * [getPrecursor_id search and return the precursor of tabId] * @param {Number} tabId * @param {String} url [default: ''] * @return {Number} id */ async getPrecursor_id(tabId, url=''){ let id = this.tab2precursor_id.hasOwnProperty(tabId)? this.tab2precursor_id[tabId]: null; if(id==null) { //} && url.length>0 && Object.keys(this.tabs).length > 0){ //let hash_url = this._getHashCode(url.replace(new RegExp('^http(s)?:\/\/', 'g'), '')); //let hash_url = url.replace(new RegExp('^http(s)?:\/\/', 'g'), ''); //let found = []; if (this.tabID2Opener.hasOwnProperty(tabId)){ id = this.getTabOpenerID(tabId); } else { for (let i = 0; i < 5; i++) { await sleep(1000); if (this.tabID2Opener.hasOwnProperty(tabId)){ id = this.getTabOpenerID(tabId); break; } } } // for (let _tabId of Object.keys(this.tabs)) { // let tab = this.tabs[_tabId]; // if(tab.is() && tab.hasContent() && tab.get().links==undefined) console.log('no Links', tab.get()); // if (tab.is() && tab.hasContent() && tab.get().links.includes(hash_url)){ // console.log('=== @Roberto: Let me know if you see this message. I have not being able to get to this condition (legacy code?) ===') // found.push({tabId: _tabId, id: tab.get().id}) // } // }//for // if(found.length===1){ // return found[0].id // }else if(found.length>1){ // for (let e of found) { // if(this.openerTabId2tab.hasOwnProperty(e.tabId) && this.openerTabId2tab[e.tabId].includes(tabId)){ // return e.id; // } // } // id = found[found.length-1].id // } } if (id){ return id; } else { return 'TabHandler.js:getPrecursor_id:01'; } } /** * [setPrecursor_id set the precursor id of the tab-id] * @param {Number} tabId */ setPrecursor_id(tabId, id){ if (id) { this.tab2precursor_id[tabId] = id; } else { this.tab2precursor_id[tabId] = 'NO(TabHandler.js:02)'; } } /** * [closeTab // close and delete tab and hand over the page to the event function * if the parameter close and tabRemove are true, than will be clean and delete the db entry * ] * @param {Number} tabId * @param {Number} openerTabId * @param {Boolean} close [close tab] * @param {Boolean} tabRemove [remove the db entry] */ closeTab(tabId, openerTabId, close=true, tabRemove=false){ if (this.debug) console.log('-> closeTab(...)'); if(openerTabId!=null){ if(!this.openerTabId2tab.hasOwnProperty(openerTabId)) this.openerTabId2tab[openerTabId] = []; if(!this.openerTabId2tab[openerTabId].includes(openerTabId)) this.openerTabId2tab[openerTabId].push(tabId); this.tabID2Opener[tabId] = openerTabId; } if(this.tabs.hasOwnProperty(tabId)){ if(this.tabs[tabId].hasContent()) { this.setPrecursor_id(tabId, this.tabs[tabId].get().id); } if(close && !tabRemove) { this.tabs[tabId].close(page => { if(page!=null){ if (this.debug) console.log('==== Emit Event: onPage (Send Page) ===='); this.event.emit(EVENT_NAMES.page, page, false); } }); }else if(close && tabRemove){ this.closeLostTabs([tabId]) } }else{ console.log('TabId %s not found', tabId); } if (this.debug) console.log('<- closeTab(...)'); } /** * [_updateDuration update the duration of active tabs] */ async _updateDuration(){ try { console.log('_updateDuration is still being called'); let tabIds = (await this.extension.getActiveTabIds()).filter((tabId, i) => this.tabs.hasOwnProperty(tabId)); if(tabIds.length>0){ for (let id of tabIds) { this.tabs[id].updateDuration(this.UPDATE_INTERVAL_DURATION) } }//if(tabIds.length>0) } catch (err) { console.log(err); this._updateDuration(); this.event.emit('error', err, true); } } /** * [_registerTime set a timer to register the active duration] */ async registerTime(){ try { let now = +new Date(); let allTabIds = (await this.extension.getAllTabsIds()).filter((tabId, i) => this.tabs.hasOwnProperty(tabId)); if(allTabIds.length>0){ for (let id of allTabIds) { if (this.tabs[id].elapsed_timer != -1) { if (this.debug) console.log('elapsed_timer: ', this.tabs[id].elapsed_timer); if (this.debug) console.log('elapsed: ', now - this.tabs[id].elapsed_timer); this.tabs[id].updateElapsed(now - this.tabs[id].elapsed_timer); this.tabs[id].elapsed_timer = -1; } } } let activeTabIds = (await this.extension.getActiveTabIds()).filter((tabId, i) => this.tabs.hasOwnProperty(tabId)); if(activeTabIds.length>0){ for (let id of activeTabIds) { this.tabs[id].elapsed_timer = now; // set the right image on the tracking icon this.extension.resetPublicImage(); } } } catch (err) { console.log(err); this.registerTime(); this.event.emit('error', err, true); } } /** * [_onFocus add the interval time to the duration time from the tab-ids ] * @param {[type]} [tabIds=null] [description] * @return {Promise} [description] */ async _onFocus(tabIds=null){ try { this.registerTime(); } catch (e) { this.event.emit('error', e, true); console.warn(e); } } async _pushData(data, count=0){ if (this.debug) console.log('-> _pushData(...)'); if(typeof data != 'object'){ console.warn('data is no object'); }else if(this.tabs.hasOwnProperty(data.tabId)){ // console.log('onTabContent %s', data.tabId, data); // if(data.count == 1 && data.hasOwnProperty('url')){ // data.precursor_id = this.getPrecursor_id(data.tabId, data.url); // this.tabs[data.tabId].addUpdate(data) // }else if(this.tabs[data.tabId].hasContent()){ // this.tabs[data.tabId].addUpdate(data) // }else{ // // this.tabs[data.tabId].create(); // console.log('tab id %s has not content', data.tabId, data.count); // } //close the tab if the data count lower than the old count if(data.count < this.tabs[data.tabId].get().count){ this.closeTab(data.tabId, undefined); } if(!this.tabs[data.tabId].get().hasOwnProperty('precursor_id')){ data.precursor_id = await this.getPrecursor_id(data.tabId, data.url); } this.tabs[data.tabId].addUpdate(data) }else if(count <= 10){ console.log('Timeout', data, count); setTimeout(()=> this._pushData(data, count+1), 1000); }else{ console.warn('Timeout over', data); } if (this.debug) console.log('<- _pushData(...)'); } async closeLostTabs(lostIds=[]){ if(lostIds.length>0){ let id = lostIds.shift(); try { let tab = null if(!this.tabs.hasOwnProperty(id)){ tab = new Tab(this.projectId, id); await tab.init() }else{ tab = this.tabs[id]; } await tab.cleanTab(page => { if(page!=null){ if (this.debug) console.log('==== Emit Event: onPage (Send Page) ===='); this.event.emit(EVENT_NAMES.page, page, false); } }) if (this.debug) console.log('Tried to delete tab', id); await this.tabCache.deleteTab(id) if (this.debug) console.log('Delete the Tab %s', id); // console.log('clean', id) this.closeLostTabs(lostIds) } catch (err) { this.closeLostTabs(lostIds) console.log(err); } }else{ if (this.debug) console.log('Close all tabs'); } } close(){ return new Promise(async (resolve, reject) => { try { this.extension.stop(); let tabs = this.tabCache.getTabs(); this.closeLostTabs(tabs); this.tabCache.close(); this.isClose = true; resolve(); } catch (err) { reject(err) } }) } /** * [start all eventlistener for the handler] * @return {Promise} */ start(){ return new Promise(async (resolve, reject) => { try { // console.warn('START'); await this.tabCache.init(); this.isClose = false; this.tabs = {}; let tabIds = await this.extension.getAllTabsIds(); let lostIds = []; for (let id of this.tabCache.getTabsIds()) { id = parseInt(id, 10); if(!tabIds.includes(id)){ lostIds.push(id); } } // close all lost tabs this.closeLostTabs(lostIds); //create all tabs for (let id of tabIds) { this.tabs[id] = new Tab(this.projectId, id); await this.tabs[id].init(); //clean tab this.tabs[id].cleanTab(page => { if(page!=null){ this.event.emit(EVENT_NAMES.page, page, false); } }) } // On close the tab this.extension.event.on('onTabRemove', tabId => { if (this.debug) console.log('-> TabHandler.onTabRemove'); if(!this.isClose){ if (this.debug) console.log('onTabRemove', tabId); this._onFocus(); this.closeTab(tabId, undefined, true, true); } }); //create ne Tab Object this.extension.event.on('onTab', tabId => { if (this.debug) console.log('-> TabHandler.onTab'); if(!this.isClose){ if (this.debug) console.log('onTab', tabId); let tab = new Tab(this.projectId, tabId); let timeout = setTimeout(()=>{ console.warn('Timeout: Failed to create Tab'); }, 2000) tab.init().then(()=> { if (this.debug) console.log('Create Tab Object'); this.tabs[tabId] = tab clearTimeout(timeout); }).catch(err => { console.error('Failed to create Tab', err); }) } }); //on focus other tab this.extension.event.on('onFocusTab', () => { if (this.debug) console.log('-> TabHandler.onFocusTab'); if(!this.isClose){ this._onFocus() } }); //on tab update this.extension.event.on('onTabUpdate', e => { if (this.debug) console.log('-> TabHandler.onTabUpdate'); if(!this.isClose){ this._onFocus(); let will_close = false; if (this.hasOwnProperty('tabs')){ if (this.tabs.hasOwnProperty(e.tabId)){ let tab = this.tabs[e.tabId].get(); let location = this.get_location(e.tab.url); let event_url = this.get_unhashed_href(location); //let's collect the hashes if (tab && tab.hasOwnProperty('hashes')){ tab.hashes.push(location.hash); } //indicate to close the tab if the urls are different if (tab){ if (tab.url){ if (event_url != tab.url){ will_close = true; } } } } } this.closeTab(e.tabId, e.openerTabId, will_close); } if (this.debug) console.log('<- TabHandler.onTabUpdate'); }); //on tab data send this.extension.event.on('onTabContent', data => { if (this.debug) console.log('onTabContent'); if(!this.isClose){ this._pushData(data); } }); // this.extension.event.on('onFocusTab', () => { // if(!this.isClose){ // this._onFocus() // } // }); resolve(); } catch (e) { this.event.emit('error', e, true); reject(e) } }); } /** * [return a location from an url] * @return href without hashes */ get_location(event_url) { let location = document.createElement('a'); location.href = event_url; return location; } /** * [rebuild an href without hash] * @return href without hashes */ get_unhashed_href(location) { return location.protocol+'//'+ location.hostname+ (location.port?":"+location.port:"")+ location.pathname+ (location.search?location.search:""); } }