Chrome on Android : connection become dead after 30 minutes - javascript

We are building a chatroom with our own notification system without depending on GCM but with service worker + SSE.
On desktop it is fine , but on mobile android app (which uses cordova-crosswalk , chromium 53 ) .
The long running notification connection become stuck after 20-30 minutes and it is in foreground activity.
It dosen't die with an error , just not reciving data. No error at all which is very weird. No way to reconnect since we do not know if the connection is dead at all.
What would be the cleanest way? Restarting connection every 5 minutes is one idea but it is not clean.
code
runEvtSource(url, fn) {
if (this.get('session.content.isAuthenticated') === true) {
var evtSource = new EventSource(url, {
withCredentials: true
});
}}
Agressive reconnect code
var evtSource = this.runEvtSource(url, fn)
var evtSourceErrorHandler = (event) => {
var txt;
switch (event.target.readyState) {
case EventSource.CONNECTING:
txt = 'Reconnecting...';
evtSource.onerror = evtSourceErrorHandler;
break;
case EventSource.CLOSED:
txt = 'Reinitializing...';
evtSource = this.runEvtSource(url, fn)
evtSource.onerror = evtSourceErrorHandler;
break;
}
console.log(txt);
evtSource.onerror = evtSourceErrorHandler

I normally add a keep-alive layer on top of the the SSE connection. It doesn't happen that often, but sockets can die without dying properly, so your connection just goes quiet and you don't get an error.
So, one way is, inside your get data function:
if(timer)clearTimeout(timer);
timer = setTimeout(reconnect, 30 * 1000);
...process the data
In other words, if it is over 30 seconds since you last got data, reconnect. Choose a value based on the frequency of the data you send: if 10% of the time there is a 60 second gap between data events, but never a 120 second gap, then setting the time-out to something higher than 120 seconds makes sense.
You might also want to keep things alive by pushing regular messages from the server to client. This is a good idea if the frequency of messages from the server is very irregular. E.g. I might have the server send the current timestamp every 30 seconds, and use a keep-alive time-out of 45 seconds on the client.
As an aside, if this is a mobile app, bear in mind if the user will appreciate the benefit of reduced latency of receiving chat messages against the downside of reduced battery life.

Related

Migrating a chrome extension that requires a long-form websocket connection to Manifest 3 [duplicate]

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?
Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or runtime ports after a certain time, which in Chrome is 5 minutes. The inactivity timer when no such requests or ports are open is even shorter: 30 seconds.
Chromium team currently considers this behavior intentional and good, however this only applies to extensions that observe infrequent events, so they'll run just a few times a day thus reducing browser memory footprint between the runs e.g. 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 socket 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 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
// send a message every 20 sec to service worker
setInterval(() => {
chrome.runtime.sendMessage({ keepAlive: true });
}, 20000);
background.js
// create the offscreen document if it doesn't already exist
async function createOffscreen() {
if (await chrome.offscreen.hasDocument?.()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['BLOBS'],
justification: 'keep service worker running',
});
}
chrome.runtime.onStartup.addListener(() => {
createOffscreen();
});
// a message from an offscreen document every 20 second resets the inactivity timer
chrome.runtime.onMessage.addListener(msg => {
if (msg.keepAlive) console.log('keepAlive');
});
"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 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.
Future of ManifestV3
Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.
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"}]
});
If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:
manifest v3
"permissions": [
"alarms"
],
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.
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.
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']
);
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.
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.

service worker inactive issue due to which port gets closed [duplicate]

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?
Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or runtime ports after a certain time, which in Chrome is 5 minutes. The inactivity timer when no such requests or ports are open is even shorter: 30 seconds.
Chromium team currently considers this behavior intentional and good, however this only applies to extensions that observe infrequent events, so they'll run just a few times a day thus reducing browser memory footprint between the runs e.g. 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 socket 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 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
// send a message every 20 sec to service worker
setInterval(() => {
chrome.runtime.sendMessage({ keepAlive: true });
}, 20000);
background.js
// create the offscreen document if it doesn't already exist
async function createOffscreen() {
if (await chrome.offscreen.hasDocument?.()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['BLOBS'],
justification: 'keep service worker running',
});
}
chrome.runtime.onStartup.addListener(() => {
createOffscreen();
});
// a message from an offscreen document every 20 second resets the inactivity timer
chrome.runtime.onMessage.addListener(msg => {
if (msg.keepAlive) console.log('keepAlive');
});
"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 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.
Future of ManifestV3
Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.
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"}]
});
If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:
manifest v3
"permissions": [
"alarms"
],
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.
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.
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']
);
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.
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.

chrome-extension mv3 disconnect after about 5 mins [duplicate]

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?
Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or runtime ports after a certain time, which in Chrome is 5 minutes. The inactivity timer when no such requests or ports are open is even shorter: 30 seconds.
Chromium team currently considers this behavior intentional and good, however this only applies to extensions that observe infrequent events, so they'll run just a few times a day thus reducing browser memory footprint between the runs e.g. 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 socket 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 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
// send a message every 20 sec to service worker
setInterval(() => {
chrome.runtime.sendMessage({ keepAlive: true });
}, 20000);
background.js
// create the offscreen document if it doesn't already exist
async function createOffscreen() {
if (await chrome.offscreen.hasDocument?.()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['BLOBS'],
justification: 'keep service worker running',
});
}
chrome.runtime.onStartup.addListener(() => {
createOffscreen();
});
// a message from an offscreen document every 20 second resets the inactivity timer
chrome.runtime.onMessage.addListener(msg => {
if (msg.keepAlive) console.log('keepAlive');
});
"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 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.
Future of ManifestV3
Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.
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"}]
});
If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:
manifest v3
"permissions": [
"alarms"
],
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.
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.
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']
);
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.
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.

Syncing Events between 2 JS apps

I have a game app (Electron) and a web app (testing on android chrome). That pass messages via a websocket server. I want to coordinate a countdown between the 2 processes. It doesn't have to be perfect but What I've observed is that if I run in an environment with low latency it's fine. But the more lag their is in the system it seems like the Electron app tries to start far earlier then the web app. I've tested all my math and it should synchronize but it just doesn't.
First the web app iniates the start of the countdown by passing a starting time to the game app
const timeToGameStart:number = peerConnection.timeToGameStart(); // time to game start = 3 x (the longest time it toke to pass a previous msg from game app to web app)
const currUnixTime:number = peerConnection.currUnixTime();
const startGameTime:number = currUnixTime + timeToGameStart;
const startGame:StartGame = <StartGame>{
msg_data_type:Msg_Data_Type.StartGame,
game_start_time:startGameTime
}
peerConnection.passMsg(startGame);
setTimeout(timer.start, timeToGameStart);
Below is the app portion of the code that responds to the msg passed to the server
const gameStartTime:number = (<StartGame> msgData).game_start_time;
const currUnixTime:number = ServerConnection.currUnixTime();
// if we are on time, wait till its right time else if we are late, start at the next inc 3,2,1
const countDownLength:number = 3;
if (currUnixTime <= gameStartTime) {
setTimeout(()=>startCountDown(countDownLength), currUnixTime - gameStartTime);
} else {
const timeWeAreLateBy:number = currUnixTime - gameStartTime;
const timeWeAreLateByInSec:number = Math.ceil(timeWeAreLateBy / 1000);
const shortCountDownLen:number = Math.max(countDownLength - timeWeAreLateByInSec, 0);
const timeToNextSec:number = Math.max((1000 * timeWeAreLateByInSec) - timeWeAreLateBy, 0);
setTimeout(()=>startCountDown(shortCountDownLen), timeToNextSec);
}
The problem is that these two separate processes are on separate OSs. Which both have a different definition of time. i.e. (new Date()).getTime() return different numbers. the difference was 2 seconds so the controller thought their was no lag in the connection and was telling the app to start ASAP.
The solution was I had to define a consistent measurement of time. After each app connects to the server they sync their time with the server by making a request for the time the server has.
I didn't need a super precise time sync so I used a simple algorithm that got the job done. The algorithm was trying to calculate the difference in time the process was off from the server. The formula I used was server_time_diff = server_time - (received_time - RTT/2). Now to get unified (or server time) you just need to call new Date() + server_time_diff. RTT is the time it took to request the time from the server.
Any improvement ideas to my algorithm is welcome.

Node.js / Server.js socket implementation problems

Having a hard time implementing a node.js/server.js setup
I'm a bit stuck right now, and hoping someone can shed some light. I'm relatively new to sockets in general, but have been programming in javascript on and off for several years, although only about as deep as is necessary to accomplish the task at hand. As a result, my understanding of some of the concepts surrounding the javascript stack heap, and sockets in general are somewhat limited.
Ok Here's the situation:
I've created an application intended to simply increment a counter, on several machines. Several users can click the "next" button and it will update instantly on all machines.
When you first connect, it retrieves the current number, and spits it out locally.
I've created the server here:
var io = require("socket.io");
var sockets = io.listen(8000);
var currentlyServing=0;
sockets.on("connection", function (socket)
{
console.log("client connected");
socket.emit("receive", currentlyServing);
socket.on("update", function(serving)
{
currentlyServing=serving;
if(currentlyServing>100)
currentlyServing=0;
if(currentlyServing<0)
currentlyServing=99;
socket.broadcast.emit("receive", currentlyServing);
console.log("update received: "+currentlyServing);
});
});
console.log("Server Started");
Here is the relevant (I hope) excerpt from the client side:
var socket = io.connect("http://www.sampledomain.com:8000");
//function to update the page when a new update is received
socket.on("receive", function(receivedServing)
{
document.getElementById('msgs').value=""+String("00" + receivedServing).slice(-2);
document.getElementById('nowServing').value=receivedServing;
});
//this is called in an onClick event in the HTML source
//sends the new number to all other stations except this one (handled by server side)
function nextServing()
{
var sendServing = parseInt(document.getElementById('nowServing').value)+1;
socket.emit("update", sendServing);
document.getElementById('nowServing').value=sendServing;
document.getElementById('msgs').value=""+String("00" + sendServing).slice(-2);
}
Ok so here's my problem. This runs absolutely fine in every system I've put it in, smoothly and beautifully - except for IE8. If left alone for more than 2-3 minutes (with no activity at all), I eventually receive a "stack overflow" error. The line number it appears on fluctuates (haven't determined the factors involved yet), but it always happens at that interval. On some workstations it takes longer, which I'm beginning to think has a direct correlation to the amount of phsyical RAM the machine has, or at least how much is being allocated to the web browser.
I found an online function to determine "max stack size", which I realize is not an exact science, however I did consistently get a number in the area of 3000. On my IE11 machine with considerable more resources, I found it to be in the area of 20,000. This may not be relevant, but I figured the more info the better :)
To avoid this problem for now so that the end users don't see this error message, I've take the entire client script, and put it into an iFrame which reloads itself every 60 seconds,essentially resetting the stack, which feels so dirty sitting so close to a web socket, but has bought me the time to post here. I've googled until I can't google any more, but when you search "node.js" or "socket.io" along with "stack overflow" on google, you just get a lot of posts about the two topics that are hosted on the stackoverflow dot com website. ARG lol
Anyone?
EDIT ON NOVEMBER 18TH 2014 AS PER COMMENTS BELOW:
the error message is most often claiming stack overflow at line 1056. IE Developer tools points towards the file socket.io.js. Line 1056 is:
return fn.apply(obj, args.concat(slice.call(arguments)));
which is insdie this section of the file:
var slice = [].slice;
/**
* Bind `obj` to `fn`.
*
* #param {Object} obj
* #param {Function|String} fn or string
* #return {Function}
* #api public
*/
module.exports = function(obj, fn){
if ('string' == typeof fn) fn = obj[fn];
if ('function' != typeof fn) throw new Error('bind() requires a function');
var args = slice.call(arguments, 2);
return function(){
return fn.apply(obj, args.concat(slice.call(arguments)));
}
};
From what I've read it seems that the problem on IE8 might be related to flash. It IE8 uses flashsocket as the default configuration. I suggest to try the following on the client side:
if(navigator.appName.indexOf("Internet Explorer")!=-1 && navigator.appVersion.indexOf("MSIE 8")==-1 ){
socket = io.connect("http://www.sampledomain.com:8000", {
transports: ['xhr-polling']
});
}
else
{
socket = io.connect("http://www.sampledomain.com:8000" );
}
This should make IE8 use long polling while all other machines use the best method they can.
On a side note: You might also want to consider incrementing the "serving" variable on the server.
Find existing issue Causes a "Stack Overflow" in IE8 when using xhr-polling #385.
This was fixed by disabling Flash.
Also find Safari over windows client use xhr-polling instead of websocket - performance are severely harm #1147. While this is Safari it may apply to IE8 because it is using similar mechanism.
I did a small test using your socket.io but in IE 10 and emulated IE8
so that I could debug well. Started capturing Network in the tab and
noticed the requests logging every few seconds.Left alone for few
minutes and I see a lot of requests logged in. You will not see this
in Chrome because it has true WebSockets. While IE8 does not support
WebSockets socket.io emulate that using plain HTTP GET/POST using some
mechanism. So my theory is that even if socket.io works with IE8 it
does not reliably emulate web sockets
My advice is to rule out IE 8 for long running client application. IE8 is no longer supported by Microsoft.
maybe try to replace
""+String("00" + receivedServing).slice(-2)
with
('00' + receivedServing).slice(-2)

Categories