4

Service workers replace background pages in Manifest v3 Google Chrome Extensions, and I’m trying to use one for my extension. The documentation says timers (setTimeout and setInterval) should be avoided because timers get canceled when service workers terminate (which may happen at any time). Unlike background pages, service workers can’t be persistent and will always be subject to termination. The documentation recommends using the Alarms API instead.

I need to be able to run a function periodically in a Manifest v3 service worker, but alarms don’t suit my use case because they’re slow. The shortest allowed alarm duration is 1 minute, but I want to run a function in shorter intervals—a second or a few seconds.

I tried using event.waitUntil to keep the service worker alive per this Stack Overflow answer. Here’s my attempted solution at mimicking a 1-second setInterval with repeating waitUntil calls:

var e;

function doThisEachSecond(resolver) {

    console.log('running function')

    // … CODE …

    e.waitUntil(new Promise(resolve => setTimeout(() => {doThisEachSecond(resolve);}, 1000)));
    resolver();
}

self.onactivate = event => {

    event.waitUntil(new Promise(resolve => {
        e = event;
        doThisEachSecond(resolve);
    }));

    console.log('activated');
};

It has big problems :

Since event.waitUntil makes the event dispatcher wait, these events don’t fire when my bootleg setInterval is running:

chrome.tabs.onUpdated.addListener(…);
chrome.tabs.onReplaced.addListener(…);
chrome.runtime.onMessage.addListener(…);

And I found that doThisEverySecond eventually stops getting called if the service worker is left to run for some time like a few minutes. When this occurs, the events that queued up during waitUntil periods fire all at once.

Looks like my approach isn’t the way to go.

How do you call a function every second in a Chrome Extension Manifest V3 Background Service Worker?

clickbait
  • 2,818
  • 1
  • 25
  • 61

1 Answers1

8

I'm really sorry, but this isn't possible.

Now that we've got the official response out of the way: I don't have a good solution for you, but perhaps a hacky one will work?

I know you can create a named alarm in chrome, and have the onAlarm call a desired function. I reviewed the documentation and it doesn't seem like there's a limit to the number of named alarms you can create; perhaps you can create 59 chrome.alarms with an offset of 1000ms each?

Oh wait, you can't use setTimeout in a service worker. That's ok, I'll just make a brute-force timeout to set it up.

function asyncDelay(msToDelay) {
  return new Promise((success, failure) => {
      var completionTime = new Date().getTime() + msToDelay
      while (true) {
        if (new Date().getTime() >= completionTime){
          success()
          break
        }
      }
      failure()
  })
}

Ok, so now I need to set up the 60 timers so they all fire 1 second after another. Don't forget to call your tick() function while setting it up or it won't start running right away.

async function run() {
  for(let i = 0; i < 60; i++) {
    tick()
    await asyncDelay(1000)
    chrome.alarms.create(`recurring-polling-thread-${i}`, { periodInMinutes:1 })
  }
}

Then we just need to trigger the tick when each alarm is finished.

chrome.alarms.onAlarm.addListener(() => {
  tick()
})

And some kind of behavior in the tick function to confirm it's working...

async function tick() {
  console.log(new Date().getTime())
}

Tada! Now tick() is called every 1000 ms!

Ahh crap.

background.js:1 1647382803613
background.js:1 1647382804613
background.js:1 1647382805613
(8) background.js:1 1647382806614
(13) background.js:1 1647382806615
background.js:1 1647382807617
background.js:1 1647382808627
background.js:1 1647382809642

Ok, so it kind of worked, but some of the alarms fire at the same time. Reviewing the documentation suggests that a 1 second granularity is not respected:

In order to reduce the load on the user's machine, Chrome limits alarms to at most once every 1 minute but may delay them an arbitrary amount more. That is, setting delayInMinutes or periodInMinutes to less than 1 will not be honored and will cause a warning. when can be set to less than 1 minute after "now" without warning but won't actually cause the alarm to fire for at least 1 minute.

In my testing locally, I've been running this service worker without it going to sleep for ~10 minutes, and it's mostly ticking every second. It's not perfect, but I'm gonna consider this successful enough to submit.

Code is here: https://github.com/EyeOfMidas/second-tick-extension

Justin Gilman
  • 479
  • 4
  • 12
  • Have you been able to test your code in a browser with several active extensions and\or more open tabs that require many resources in terms of cpu or memory? I believe that in these cases many alarms could go lost and other delayed by many ms. I agree that if the end purpose is to trigger more alarms in a minute and we do not expect a Swiss watch then everything is more than valid. – Robbi Mar 17 '22 at 22:37
  • I have not. The fact that alarms can be delayed an arbitrary amount means that there's really not a serviceworker-approved way to get sub-minute events consistently; even the `chrome.alarm` can be delayed several seconds if browser resources are occupied elsewhere. You can experiment around with the code I shared on github, or give me an example of a "realistic" behavior this `tick()` function should be doing and I can try some benchmarks. – Justin Gilman Mar 18 '22 at 14:45