89

I need to define my Service Worker as persistent in my Chrome extension because I'm using the webRequest API to intercept some data passed in a form for a specific request, but I don't know how I can do that. I've tried everything, but my Service Worker keeps unloading.

How can I keep it loaded and waiting until the request is intercepted?

Keven Augusto
  • 895
  • 1
  • 5
  • 5

8 Answers8

176

Table of contents

  • Description of the problem

  • Workarounds:

    • Bug exploit
    offscreen API
    nativeMessaging API
    WebSocket API
    chrome messaging API
    • Dedicated tab

  • Caution

Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all its activities/requests after a certain time, which in Chrome is 5 minutes. The inactivity timer (i.e. when no such activities are ongoing) is even shorter: 30 seconds.

Chromium team currently considers this behavior good (the team relaxes some aspects though occasionally e.g. Chrome 114 prolongs chrome.runtime port after each message), however this is only good for extensions that observe infrequent events, which run just a few times a day thus reducing browser's memory footprint between the runs (for example, webRequest/webNavigation events with urls filter for a rarely visited site). These extensions can be reworked to maintain the state, example. Unfortunately, such an idyll is unsustainable in many cases.

Known problems

  • Problem 1: Chrome 106 and older doesn't wake up SW for webRequest events.

    Although you can try to subscribe to an API like chrome.webNavigation as shown in the other answers, but it helps only with events that occur after the worker starts.

  • Problem 2: the worker randomly stops waking up for events.

    The workaround may be to call chrome.runtime.reload().

  • Problem 3: Chrome 109 and older doesn't prolong SW lifetime for a new chrome API event in an already running background script. It means that when the event occurred in the last milliseconds of the 30-second inactivity timeout your code won't be able to run anything asynchronous reliably. It means that your extension will be perceived as unreliable by the user.

  • Problem 4: worse performance than MV2 in case the extension maintains a remote connection or the state (variables) takes a long time to rebuild or you observe frequent events like these:

    • chrome.tabs.onUpdated/onActivated,
    • chrome.webNavigation if not scoped to a rare url,
    • chrome.webRequest if not scoped to a rare url or type,
    • chrome.runtime.onMessage/onConnect for messages from content script in all tabs.

    Starting SW for a new event is essentially like opening a new tab. Creating the environment takes ~50ms, running the entire SW script may take 100ms (or even 1000ms depending on the amount of code), reading the state from storage and rebuilding/hydrating it may take 1ms (or 1000ms depending on the complexity of data). Even with an almost empty script it'd be at least 50ms, which is quite a huge overhead to call the event listener, which takes only 1ms.

    SW may restart hundreds of times a day, because such events are generated in response to user actions that have natural gaps in them e.g. clicked a tab then wrote something, during which the SW is terminated and restarted again for a new event thus wearing down CPU, disk, battery, often introducing a frequent perceivable lag of the extension's reaction.

"Persistent" service worker via bug exploit

Chrome 110 introduced a bug: calling any asynchronous chrome API keeps the worker running for 30 seconds more. The bug is not yet fixed.

// background.js

const keepAlive = () => setInterval(chrome.runtime.getPlatformInfo, 20e3);
chrome.runtime.onStartup.addListener(keepAlive);
keepAlive();

"Persistent" service worker with offscreen API

Courtesy of Keven Augusto.

In Chrome 109 and newer you can use offscreen API to create an offscreen document and send some message from it every 30 second or less, to keep service worker running. Currently this document's lifetime is not limited (only audio playback is limited, which we don't use), but it's likely to change in the future.

  • manifest.json

      "permissions": ["offscreen"]
    
  • offscreen.html

    <script src="offscreen.js"></script>
    
  • offscreen.js

    setInterval(async () => {
      (await navigator.serviceWorker.ready).active.postMessage('keepAlive');
    }, 20e3);
    
  • background.js

    async function createOffscreen() {
      await chrome.offscreen.createDocument({
        url: 'offscreen.html',
        reasons: ['BLOBS'],
        justification: 'keep service worker running',
      }).catch(() => {});
    }
    chrome.runtime.onStartup.addListener(createOffscreen);
    self.onmessage = e => {}; // keepAlive
    createOffscreen();
    

"Persistent" service worker while nativeMessaging host is connected

In Chrome 105 and newer the service worker will run as long as it's connected to a nativeMessaging host via chrome.runtime.connectNative. If the host process is terminated due to a crash or user action, the port will be closed, and the SW will terminate as usual. You can guard against it by listening to port's onDisconnect event and call chrome.runtime.connectNative again.

"Persistent" service worker while WebSocket is active

Chrome 116 and newer: exchange WebSocket messages less than every 30 seconds to keep it active, e.g. every 25 seconds.

"Persistent" service worker while a connectable tab is present

Downsides:

  • The need for an open web page tab
  • Broad host permissions (like <all_urls> or *://*/*) for content scripts which puts most extensions into the slow review queue in the web store.

Warning! If you already connect ports, don't use this workaround, use another one for ports below.

Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.

  • manifest.json, the relevant part:

      "permissions": ["scripting"],
      "host_permissions": ["<all_urls>"],
      "background": {"service_worker": "bg.js"}
    
    
  • background service worker bg.js:

    const onUpdate = (tabId, info, tab) => /^https?:/.test(info.url) && findTab([tab]);
    findTab();
    chrome.runtime.onConnect.addListener(port => {
      if (port.name === 'keepAlive') {
        setTimeout(() => port.disconnect(), 250e3);
        port.onDisconnect.addListener(() => findTab());
      }
    });
    async function findTab(tabs) {
      if (chrome.runtime.lastError) { /* tab was closed before setTimeout ran */ }
      for (const {id: tabId} of tabs || await chrome.tabs.query({url: '*://*/*'})) {
        try {
          await chrome.scripting.executeScript({target: {tabId}, func: connect});
          chrome.tabs.onUpdated.removeListener(onUpdate);
          return;
        } catch (e) {}
      }
      chrome.tabs.onUpdated.addListener(onUpdate);
    }
    function connect() {
      chrome.runtime.connect({name: 'keepAlive'})
        .onDisconnect.addListener(connect);
    }
    
  • all your other extension pages like the popup or options:

    ;(function connect() {
      chrome.runtime.connect({name: 'keepAlive'})
        .onDisconnect.addListener(connect);
    })();
    

If you also use sendMessage

In Chrome 99-101 you need to always call sendResponse() in your chrome.runtime.onMessage listener even if you don't need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.

If you already use ports e.g. chrome.runtime.connect

Warning! If you also connect more ports to the service worker you need to reconnect each one before its 5 minutes elapse e.g. in 295 seconds. This is crucial in Chrome versions before 104, which killed SW regardless of additional connected ports. In Chrome 104 and newer this bug is fixed but you'll still need to reconnect them, because their 5-minute lifetime hasn't changed, so the easiest solution is to reconnect the same way in all versions of Chrome: e.g. every 295 seconds.

  • background script example:

    chrome.runtime.onConnect.addListener(port => {
      if (port.name !== 'foo') return;
      port.onMessage.addListener(onMessage);
      port.onDisconnect.addListener(deleteTimer);
      port._timer = setTimeout(forceReconnect, 250e3, port);
    });
    function onMessage(msg, port) {
      console.log('received', msg, 'from', port.sender);
    }
    function forceReconnect(port) {
      deleteTimer(port);
      port.disconnect();
    }
    function deleteTimer(port) {
      if (port._timer) {
        clearTimeout(port._timer);
        delete port._timer;
      }
    }
    
  • client script example e.g. a content script:

    let port;
    function connect() {
      port = chrome.runtime.connect({name: 'foo'});
      port.onDisconnect.addListener(connect);
      port.onMessage.addListener(msg => {
        console.log('received', msg, 'from bg');
      });
    }
    connect();
    

"Forever", via a dedicated tab, while the tab is open

Instead of using the SW, open a new tab with an extension page inside, so this page will act as a "visible background page" i.e. the only thing the SW would do is open this tab. You can also open it from the action popup.

chrome.tabs.create({url: 'bg.html'})

It'll have the same abilities as the persistent background page of ManifestV2 but a) it's visible and b) not accessible via chrome.extension.getBackgroundPage (which can be replaced with chrome.extension.getViews).

Downsides:

  • consumes more memory,
  • wastes space in the tab strip,
  • distracts the user,
  • when multiple extensions open such a tab, the downsides snowball and become a real PITA.

You can make it a little more bearable for your users by adding info/logs/charts/dashboard to the page and also add a beforeunload listener to prevent the tab from being accidentally closed.

Caution regarding persistence

You still need to save/restore the state (variables) because there's no such thing as a persistent service worker and those workarounds have limits as described above, so the worker can terminate. You can maintain the state in a storage, example.

Note that you shouldn't make your worker persistent just to simplify state/variable management. Do it only to restore the performance worsened by restarting the worker in case your state is very expensive to rebuild or if you hook into frequent events listed in the beginning of this answer.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • 2
    Thank you very much!! I spent two days thinking it was an error in my code, I just switched to MV2 and now it's working!! – Keven Augusto Mar 14 '21 at 18:03
  • So folks are aware in the "Keep alive forever via runtime ports" option the `executeScript` invocation's `ScriptInjection` requires a `function` key and NOT a `func` key (Version 90.0.4430.212). This contradicts the [ScriptInjection](https://developer.chrome.com/docs/extensions/reference/scripting/#type-ScriptInjection) documentation but does match the [scripting documentation](https://developer.chrome.com/docs/extensions/reference/scripting/#runtime-functions) – Poke Jul 09 '21 at 17:17
  • none of these approaches do what the answer purports. the `keepAlive` strategy is just redundancy and just who in their right minds would want to keep a tab open? this answer stands downvoted for these reasons. – suraj Sep 30 '21 at 07:28
  • 7
    @surajsharma, everything in this answer was tested by multiple users and does exactly what it says. – wOxxOm Sep 30 '21 at 08:48
  • 1
    @wOxxOm I see you all over regarding the persistent v3 service worker. I am having an issue I am having trouble fixing. I'm using your script. The keep alive functions maybe 3 or 4 times then my background service worker restarts itself. I have a file like `console.log("start")`, then all ur keep alive stuff (added logs), then my actual backgroundController logic. I will get a log that the timeout is running and keepAlive port is reconnecting. This happens maybe 3 or 4 times. Then a log `start` and a run-through of all my background app setup again, indicating a restart of the service worker. – diabetesjones Feb 23 '22 at 03:29
  • Continued: is there anything you can think of that would be causing this to run successfully a few times over 15 to 20 mins then collapse? It's very important to my extension that the bg controller does not restart randomly, it holds state and such, and communicates via the content script. – diabetesjones Feb 23 '22 at 03:31
  • 1
    Please don't use comments to ask questions. Post a new question with your real actual code ([MCVE](/help/mcve)). – wOxxOm Feb 23 '22 at 05:39
  • 1
    Thanks for your answer! While you have correctly mentioned "Persistent service worker while a **connectable** tab is present", I feel you could expand on that in the "Downsides" section of your answer also. This is because at first I thought the approach works as long as the browser window has _any_ website tabs open. But it does not work in the specific case where the browser has _only_ non-connectable tabs open, such as CWS pages or the new tab page. So, if a user opens example.com, then navigates to new tab, and then goes to google.com after some time, the SW will have restarted. – Gaurang Tandon Jun 01 '22 at 08:26
  • 2
    This technique seems to be working failry stably for me - and importantly works for my usecase. Thank you @wOxxOm. I've been reading much of your contributions in the google groups forum and agree with your stand point. How likely do you think it is that the extensions team might decide to plug this "loop hole"? – jflood.net Jul 07 '22 at 23:40
  • 2
    AFAIK it can't be plugged. – wOxxOm Jul 08 '22 at 04:42
  • 1
    thanks @wOxxOm for the workaround, I can confirm that is working fine. I have a question regarding approval process. are chrome store will allow this workaround in the review process? –  Aug 08 '22 at 08:19
  • Regarding the 'Warning for Chrome versions before 104' for runtime.connect - what changed in 104 here? I've been finding it difficult to get a precise picture of the current behaviour of service worker lifetimes. – user3896248 Aug 30 '22 at 09:44
  • @user3896248, Chrome 104+ no longer starts the kill timer when the current port's 5 minute lifetime ends if there are other still connected ports: crrev.com/1011741. – wOxxOm Aug 30 '22 at 10:14
  • @wOxxOm At first glance I thought this applied exclusively to native messaging hosts and not to content scripts / extension page scripts - but it looks like all kinds of messaging port might disable the timeout? That's good to know, thanks a lot. – user3896248 Aug 30 '22 at 10:23
  • 1
    The change in the code is not specific to nativeMessaging, it affects every external connection including runtime ports. That said, each port's lifetime is still limited to 5 minutes, so we still need to reconnect them. I've remove the v104 notice from the answer. – wOxxOm Aug 30 '22 at 10:27
  • I tried all these soultions but except nativemessaging one nothing works properly, these solutions are not 100% reliable. I tried opening a new window from service worker and reconnecting it after 4 minutes 55 seconds, it worked but still randomly service worker turns inactive. So all these solutions are not full proof. – Manoj Verma Oct 29 '22 at 15:04
  • As long as the other tab/window is open and as long as you account for every comm channel/port, it works reliably - barring a bug in Chrome. Recently I had to chase down an elusive port in one of my extensions and it wasn't easy but I did it eventually after a lot of debugging. – wOxxOm Oct 29 '22 at 15:59
  • does the "via dedicated tab, while the tab is open" method work for anybody? my extension says the open page is active, but still makes the service worker inactive after some time. google chrome version is 108.0.5359.125 official build. – winwin Dec 21 '22 at 10:40
  • 1
    @winwin, the point is to use the dedicated tab as a replacement for SW, instead of SW. – wOxxOm Dec 21 '22 at 11:12
  • @wOxxOm oh yeah didn't read it thoroughly, thanks! – winwin Dec 21 '22 at 11:22
  • It seems future service workers of extensions will persist based on it's activity https://developer.chrome.com/docs/extensions/mv3/known-issues/#sw-fixed-lifetime – Oshan Abeykoon Jan 17 '23 at 09:59
  • 1
    @OshanAbeykoon, no, it won't persist. They only fixed one bug described as "Problem 3" in the answer, so now its lifetime will be prolonged by 30 seconds since the last chrome event as it should. – wOxxOm Jan 17 '23 at 20:21
  • From one of the chrome bug trackers linked above this comment about being able to extend indefinitely was interesting/useful: https://bugs.chromium.org/p/chromium/issues/detail?id=1152255#c184 and points to this blog post: https://developer.chrome.com/blog/longer-esw-lifetimes/ (there are apparently still 2 other related bugs being tracked though: https://bugs.chromium.org/p/chromium/issues/detail?id=1418780 and https://bugs.chromium.org/p/chromium/issues/detail?id=1414879 – Glenn 'devalias' Grant Jun 18 '23 at 07:36
  • There's also this bug that was marked as fixed 9 days ago that relates to websocket connections extending the serviceworker lifetime indefinitely beyond the 5min: https://bugs.chromium.org/p/chromium/issues/detail?id=1382537 Not sure if it's been released yet/in what chrome version though. – Glenn 'devalias' Grant Jun 18 '23 at 07:44
  • @wOxxOm I think if you open service worker 'dev tools' (click on link on extension page) and keep that page open, it won't go to sleep ever...? At least looking at it's console tab – Andrew Jul 01 '23 at 21:20
  • I see exactly that Andrew - while I am debugging the devtools the service worker stays alive and my extension works. But the minute I go to production mode and close it, it dies. This is very tricky. – httpete Jul 13 '23 at 19:00
  • Thanks, very useful. It would be interesting to get your perspective on a cross-browser solution. I've just tried running my MV3 SW on Firefox and discovered that it doesn't support SWs. Too long for a comment, obviously, but any links would be helpful. – EML Aug 26 '23 at 09:52
10

unlike the chrome.webRequest API the chrome.webNavigation API works perfectly because the chrome.webNavigation API can wake up the service worker, for now you can try putting the chrome.webRequest API api inside the chrome.webNavigation.

chrome.webNavigation.onBeforeNavigate.addListener(function(){

   chrome.webRequest.onResponseStarted.addListener(function(details){

      //.............
      
      //.............

   },{urls: ["*://domain/*"],types: ["main_frame"]});


},{
    url: [{hostContains:"domain"}]
});
7

If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:

  1. manifest v3
"permissions": [
    "alarms"
],
  1. service worker background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
  console.log('log for debug')
});

Unfortunately this is not my problem and may be you have different problem too. When i refresh dev extension or stop and run prod extension some time service worker die at all. When i close and open browser worker doesn't run and any listeners inside worker doesn't run it too. It tried register worker manually. Fore example:

// override.html
<!DOCTYPE html>
<html lang="en">

  <head>...<head>
  <body>
    ...
    <script defer src="override.js"></script>
  <body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
  for (let worker of res) {
    console.log(worker)
    if (worker.active.scriptURL.includes('background.js')) {
      return
    }
  }

  navigator.serviceWorker
    .register(chrome.runtime.getURL('background.js'))
    .then((registration) => {
      console.log('Service worker success:', registration)
    }).catch((error) => {
      console.log('Error service:', error)
    })
})

This solution partially helped me but it does not matter because i have to register worker on different tabs. May be somebody know decision. I will pleasure.

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Neeraj Nov 17 '21 at 11:47
  • 1
    Technically, this is answer is not related to persistence of the background script but it still provides a workaround for an inherent bug in ManifestV3 where the background script gets lost altogether during an update, https://crbug.com/1271154. – wOxxOm Nov 26 '21 at 07:26
4

I found a different solution to keeping the extension alive. It improves on wOxxOm's answer by using a secondary extension to open the connection port to our main extension. Then both extensions try to communicate with each other in the event that any disconnects, hence keeping both alive.

The reason this was needed was that according to another team in my company, wOxxOm's answer turned out to be unreliable. Reportedly, their SW would eventually fail in an nondeterministic manner.

Then again, my solution works for my company as we are deploying enterprise security software, and we will be force installing the extensions. Having the user install 2 extensions may still be undesirable in other use-cases.

LetsDoThis
  • 95
  • 6
  • 4
    This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/31288692) – Tom Mar 18 '22 at 09:31
3

As Clairzil Bawon samdi's answer that chrome.webNavigation could wake up the service worker in MV3, here are workaround in my case:

// manifest.json
...
"background": {
  "service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...

In my case it listens onHistoryStateUpdated event to wake up the service worker:

// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
  console.log('wake me up');
});

chrome.webRequest.onSendHeaders.addListener(
  (details) => {
    // code here
  },
  {
    urls: ['https://example.com/api/*'],
    types: ['xmlhttprequest'],
  },
  ['requestHeaders']
);
Agung Darmanto
  • 419
  • 3
  • 9
  • This did it for me, thanks a lot! Very simple and to the point (in my case I need to capture some data from a certain website when the user navigates it, so I just added the onHistoryStateUpdated and the rest remained the same) – pimguilherme May 21 '22 at 18:16
  • Can you please explain this code more? What is the onSendHeaders step for? And do we all need to enable host_permissions to make this work? Or, would simply adding webNavigation in the manifest and then doing the onHistoryStateUpdated step be sufficient enough? – Volomike Jul 14 '22 at 14:35
2

IMHO (and direct experience) a well structured SW will work forever.

Obviously there are some particular cases, like uninterruptible connections, which may suffer a lot once SW falls asleep, but still if the code is not prepared to handle the specific behaviour.

It seems like a battle against windmills, punctually after 30 seconds SW stops doing anything, falls asleep, several events are not honored anymore and the problems start... if our SW has nothing else pressing to think about.

From "The art of War" (Sun Tzu): if you can't fight it, make friends with it.

so... ok, lets try to give something consistent to think about from time to time to our SW and put a "patch" (because this IS A PATCH!) to this issue.

Obviously I don't assure this solution will work for all of you, but it worked for me in the past, before I decided to review the whole logic and code of my SW.

So I decided to share it for your own tests.

  • This doesn't require any special permission in manifest V3.
  • Remember to call the StayAlive() function below at SW start.
  • To perform reliable tests remember to not open any DevTools page. Use chrome://serviceworker-internals instead and find the log (Scope) of your extension ID.

EDIT:

Since the logic of the code may not be clear to some, I will try to explain it to dispel doubts:

Any extension's SW can attempt to make a connection and send messages through a named port and, if something fails, generate an error.

The code below connects to a named port and tries to send a message through it to a nonexistent listener (so it will generate errors).

While doing this, SW is active and running (it has something to do, that is, it has to send a message through a port).

Because noone is listening, it generates a (catched and logged) error (in onDisconnect) and terminates (normal behaviour happening in whatever code).

But after 25 secs it does the same iter from start, thus keeping SW active forever.

It works fine. It is a simple trick to keep the service worker active.

    // Forcing service worker to stay alive by sending a "ping" to a port where noone is listening
    // Essentially it prevents SW to fall asleep after the first 30 secs of work.
    
    const INTERNAL_STAYALIVE_PORT = "Whatever_Port_Name_You_Want"
    var alivePort = null;
    ...
    StayAlive();
    ...
    
    async function StayAlive() {
    var lastCall = Date.now();
    var wakeup = setInterval( () => {
        
        const now = Date.now();
        const age = now - lastCall;            
        
        console.log(`(DEBUG StayAlive) ----------------------- time elapsed: ${age}`)
        if (alivePort == null) {
            alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})

            alivePort.onDisconnect.addListener( (p) => {
                if (chrome.runtime.lastError){
                    console.log(`(DEBUG StayAlive) Disconnected due to an error: ${chrome.runtime.lastError.message}`);
                } else {
                    console.log(`(DEBUG StayAlive): port disconnected`);
                }

                alivePort = null;
            });
        }

        if (alivePort) {
                        
            alivePort.postMessage({content: "ping"});
            
            if (chrome.runtime.lastError) {                              
                console.log(`(DEBUG StayAlive): postMessage error: ${chrome.runtime.lastError.message}`)                
            } else {                               
                console.log(`(DEBUG StayAlive): "ping" sent through ${alivePort.name} port`)
            }
            
        }         
        //lastCall = Date.now();             
        
    }, 25000);
}

Hoping this will help someone.

Anyway, I still recommend, where possible, to review the logic and the code of your SW, because, as I mentioned at the beginning of this post, any well structured SW will work perfectly in MV3 even without tricks like this one.

EDIT (jan 17, 2023)

when you think you've hit bottom, watch out for the trapdoor that might suddenly open under your feet. Sun Tzu

This revision of the StayAlive() function above still keeps the service worker active, but avoids calling the function every 25 seconds, so as not to burden it with unnecessary work.

In practice, it appears that by running the Highlander() function below at predefined intervals, the service worker will still live forever.

How it works

The first call of Highlander() is executed before the expiration of the fateful 30 seconds (here it is executed after 4 seconds from the start of the service worker).

Subsequent calls are performed before the expiration of the fateful 5 minutes (here they are executed every 270 seconds).

The service worker, in this way, will never go to sleep and will always respond to all events.

It thus appears that, per Chromium design, after the first Highlander() call within the first 30 seconds, the internal logic that manages the life of the (MV3) service worker extends the period of full activity until the next 5 minutes.

This is really really hilarious...

anyway... this is the ServiceWorker.js I used for my tests.

// -----------------
// SERVICEWORKER.JS
// -----------------

const INTERNAL_STAYALIVE_PORT = "CT_Internal_port_alive"
var alivePort = null;

const SECONDS = 1000;
var lastCall = Date.now();
var isFirstStart = true;
var timer = 4*SECONDS;
// -------------------------------------------------------
var wakeup = setInterval(Highlander, timer);
// -------------------------------------------------------
    
async function Highlander() {

    const now = Date.now();
    const age = now - lastCall;
    
    console.log(`(DEBUG Highlander) ------------- time elapsed from first start: ${convertNoDate(age)}`)
    if (alivePort == null) {
        alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})

        alivePort.onDisconnect.addListener( (p) => {
            if (chrome.runtime.lastError){
                console.log(`(DEBUG Highlander) Expected disconnect (on error). SW should be still running.`);
            } else {
                console.log(`(DEBUG Highlander): port disconnected`);
            }

            alivePort = null;
        });
    }

    if (alivePort) {
                    
        alivePort.postMessage({content: "ping"});
        
        if (chrome.runtime.lastError) {                              
            console.log(`(DEBUG Highlander): postMessage error: ${chrome.runtime.lastError.message}`)                
        } else {                               
            console.log(`(DEBUG Highlander): "ping" sent through ${alivePort.name} port`)
        }            
    }         
    //lastCall = Date.now();
    if (isFirstStart) {
        isFirstStart = false;
        clearInterval(wakeup);
        timer = 270*SECONDS;
        wakeup = setInterval(Highlander, timer);
    }        
}

function convertNoDate(long) {
    var dt = new Date(long).toISOString()
    return dt.slice(-13, -5) // HH:MM:SS only
}

EDIT (jan 20, 2023):

On Github, I created a repository for a practical example of how to properly use the Highlander function in a real world extension. For the implementation of this repo, I also took into account wOxxOm's comments to my post (many thanks to him).

Still on Github, I created another repository to demonstrate in another real world extension how a service worker can immediately start by itself (put itself in RUNNING status), without the aid of external content scripts, and how it can live on forever using the usual Highlander function. This repository includes a local WebSocket Echo Test server used by the extension in its client communication sample and useful to externally debug the extension when the extension's host browser has been closed. That's right, because, depending on the type of configuration applied, when the host browser is closed Highlander-DNA can either shut down with the browser or continue to live forever, with all functionality connected and managed (e.g. the included WebSocket client/server communications test sample).

EDIT (jan 22, 2023)

I tested memory and CPU consumption while a Service Worker is always in RUNNING state due to the use of Highlander. The consumption to keep it running all the time is practically ZERO. I really don't understand why the Chromium team is persisting in wanting to unnecessarily complicate everyone's life.

radiolondra
  • 293
  • 4
  • 15
  • It won't work without the other page/context listening via chrome.runtime.onConnect. If this works for you as-is, the only explanation is a bug in Chrome which will be eventually fixed. – wOxxOm Jan 11 '23 at 13:12
  • @wOxxOm It works for me as it is. Latest Chrome version. How did you tested it? Did you have errors? Could you give me the source of your test extension? – radiolondra Jan 11 '23 at 13:28
  • @wOxxOm maybe you missed the logic of this code. Any extension can attempt to make a connection and if the connection fails, generate an error. The code here does the same and works fine. It is not a Chrome error, as you suppose, but a stratagem to keep the service worker active. Try to believe. – radiolondra Jan 11 '23 at 14:33
  • My point was that if Chrome keeps the SW alive for a port that wasn't really connected it is a bug that will be eventually fixed. – wOxxOm Jan 11 '23 at 17:32
  • @wOxxOm As per documentation about chrome.runtime.connect: "Attempts to connect listeners within an extension/app (such as the background page), or other extensions/apps. ... The port's onDisconnect event is fired if the extension/app does not exist." This is what is using the code, the whole trick is exactly there, it exploits precisely the OnDisconnect event fired because noone is listening (pratically, as the documentation says, "the extension/app doesn't exists"). Where is the Chrome bug you are speaking about? – radiolondra Jan 11 '23 at 17:50
  • Per the service worker specification it is kept alive only for events that were initiated externally or for connections to an external client. Prolonging the lifetime via self-initiated activities is a bug. There were such bugs in the past and they were fixed. This one will be fixed eventually as well. – wOxxOm Jan 11 '23 at 18:25
  • For the sake of precision, I filed a post to the chromium error team, reporting the problem. https://bugs.chromium.org/p/chromium/issues/detail?id=1406613 Awaiting response. – radiolondra Jan 15 '23 at 09:54
  • darn but it works consistently for now – AEQ Jan 15 '23 at 22:38
  • I am happy to hear that. We will wait until February to get the 110 version without all these problems, finally! Thank you so much. – radiolondra Jan 25 '23 at 11:01
  • it is really working. It would be really great if you could clean the code a bit to make it production ready. Also if you can post a few screen shots about its performance. – SkyRar Mar 10 '23 at 16:36
0
setInterval(()=>{self.serviceWorker.postMessage('test')},20000)

No idea how this works, but it seems to keep the service worker awake on Chrome 100, 105, 108, and 114. Haven't tested on other versions.

derder56
  • 111
  • 1
  • 8
-3

WebSocket callbacks registered from within the chrome.runtime listener registrations of my extensions's service worker would not get invoked, which sounds like almost the same problem.

I approached this problem by making sure that my service worker never ends, by adding the following code to it:

function keepServiceRunning() {
    setTimeout(keepServiceRunning, 2000);
  }

keepServiceRunning()

After this, my callbacks now get invoked as expected.

matanster
  • 15,072
  • 19
  • 88
  • 167
  • This doesn't work per the specification of service workers. You must have had devtools open for the service worker, which keeps the worker active intentionally circumventing the specification's timeouts to simplify debugging. – wOxxOm Aug 14 '21 at 05:30
  • Actually, I'm slightly confused, as my suggested answer code does keep the service alive indefinitely without devtools being open. Chromium Beta Version 93.0.4577.51 (Official Build) beta (64-bit). – matanster Aug 24 '21 at 23:16
  • It means there's a bug in the browser or something in your script is using the extended 5-minute timeout e.g. ports, messages, and a few other things. – wOxxOm Aug 24 '21 at 23:33
  • Thanks, I have added my use case to [crbug.com/1152255](https://bugs.chromium.org/p/chromium/issues/detail?id=1152255#c37) then, as I'm not explicitly using the extended timeout in any way I'm aware of – matanster Aug 24 '21 at 23:56
  • Without [MCVE](/help/mcve) I can't tell what's wrong. I only verified that it doesn't work in several different versions of Chrome including 93 per the specification. Note that chrome.runtime messaging is one of things that enable the extended 5-minute timeout. – wOxxOm Aug 25 '21 at 00:15