Search code examples
javascriptgoogle-chrome-extensiontimer

Chrome extension timer parallel instances and port connection issues


I am trying to run a timer script in the background of a chrome extension. I want the timer to be running after the user has started the timer from the popup.js.

What I did: I established a port connection between the popup.js and background.js and it worked. It successfully logs the time to my popup.js console.

(The problem is summarized below).


Popup.js:

//create a port
let port = chrome.runtime.connect({name:"timeScript"});

//in this certain event send duration to background and receive the constantly updating time
play.addEventListener("click", () => {

    port.postMessage({duration: 6*60});

    port.onMessage.addListener((x) => {
        console.log(`${x.time[0]} : ${x.time[1]}`);
    });
});

Background.js:

chrome.runtime.onConnect.addListener((port) => {

    console.assert(port.name = "timeScript");
    
    //listen for messages from popup.js
    port.onMessage.addListener((x) => {
        let duration = x.duration;

        //timer logic
        function startTimer(duration) {
            var start = Date.now(), diff, minutes, seconds;
    
            function timer() {
                diff = duration - (((Date.now() - start) / 1000) | 0);
        
                minutes = (diff / 60) | 0;
                seconds = (diff % 60) | 0;
        
                minutes = minutes < 10 ? "0" + minutes : minutes;
                seconds = seconds < 10 ? "0" + seconds : seconds;
        
                if (diff <= 0) {
                    start = Date.now() + 1000;
                }
                
                //Send message to the popup.js
                port.postMessage({time: [`${minutes}`, `${seconds}`]});

            };
            timer();
            setInterval(timer, 1000);
        }
        //timer logic end

        startTimer(duration)    
    })
})

Issues:

  1. When I play the timer from my popup.js after I have already had played it once, a second timer starts running parallel to the first. If I press it the third time, a third timer will run parallel and so on.

  2. I understand that when the popup window is closed, popup.js stops running hence, the port connection is closed and throws an error: Uncaught Error: Attempting to use a disconnected port object in the background.js. I want the timer to keep running and reconnect to popup.js when it is up again. I researched on this but got nothing so far.

I think these problems are due to the 'async chrome API functions' (as I read something similar somewhere), but I am unable to point out the issue.


This is my manifest.json, in case it is required.

Manifest:

{
  "name": "Timer",
  "description": "Timer",
  "version": "0.0.0.1",
  "manifest_version": 2,
  "icons": {
    "128": "img/icon_128.png",
    "48": "img/icon_48.png",
    "16": "img/icon_16.png"
  },
  "browser_action": {
      "default_icon": "img/icon_16.png",
      "default_popup": "popup.html"
  },
  "background": {
    "page": "background.html",
    "persistent": true
  }
}

Solution

  • It's not related to "async". The problem is that your timer is using the reference to port but the popup runs only when shown so it terminates the connection when it's closed making port unusable (this is the error you see). When the popup opens again, your onConnect listener will create a new port in addition to the old one used by the old timer.

    The solution is to extract the timer to the global scope and make port global:

    let popupPort;
    
    chrome.runtime.onConnect.addListener((port) => {
      popupPort = port;
      port.onMessage.addListener((x) => {
        startTimer(x.duration);
      });
      port.onDisconnect.addListener(() => {
        popupPort = null;
      });
    });
    
    function startTimer(duration) {
      // ...............
      function timer() {
        // ...............
        if (popupPort) {
          popupPort.postMessage({time: [`${minutes}`, `${seconds}`]});
        }
      }
      // ...............
    }
    

    This is a simplified example which won't work properly if two popups are open simultaneously in two different windows. To make it work for any number of popups you'll need to convert popupPort to an array or Set, update it properly, and call postMessage on each entry.