detect offline or online using service worker - javascript

I am using service worker to check if a user is online or offline when a request is made. Here are some of the approaches I have taken:
In this method in service worker,
self.addEventListener("fetch", (event) => {
if (navigator.onLine){
}
})
navigator.onLine only works when you check/uncheck the offline checkbox in Inspect Element. But when I switch the device's internet on/off, it will always return true whether im offline or online.
And also from what i've seen in other answers, navigator.onLine will return true if you are connected to your local network even if your local network has no internet connection.
I have tried to ping a url in the self.addEventListener("fetch", {...}) method as shown here https://stackoverflow.com/a/24378589/6756827. This will bring an error in in the new XMLHttpRequest(); object.
I have tried to load an online resource (an image) in the self.addEventListener("fetch", {...}) method as shown here https://stackoverflow.com/a/29823818/6756827. The console shows an error for new Image();.
Because none of these approaches work, how do we check if a user is online or offline in service worker when a request is made ?

In the service worker you do not have access to the navigator object and thus no access to the online property or events.
Instead you know you are offline when a fetch throws an exception. The only time a fetch will throw an exception is when the device cannot connect to the Internet/Network.
self.addEventListener( "fetch", function ( event ) {
event.respondWith(
handleRequest( event )
.catch( err => {
//assume offline as everything else should be handled
return caches.match( doc_fallback, {
ignoreSearch: true
} );
} )
);
} );
For a more complex handling of the situation you can sync messages of online/offline state with the UI using the message API:
self.addEventListener( "message", event => {
//handle message here
}

Related

Service worker: background synchronization event doesn't fire on network restore [Chrome]

I have the following minor application:
(angular 13 - main thread)
home.component.ts
public sendIssueDescription(e) : void {
e.preventDefault();
if('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.ready
.then((sw:any) => {
let issue = this.getIssue();
this.dbService.saveIssue(issue)
.subscribe(
res => {
console.log("Saving a problem was Ok:" + JSON.stringify(res));
sw.sync.register('sync-new-problem');
},
err => console.log("Error ocurred:", err),
() => console.log("Syncronisation is totally completed")
);
});
}
else {
// A browser do not support a background syncronization, we need to send the data right now
let issue = this.getIssue();
this.problemReportingService.postIssue(issue).subscribe(
res => window.console.log("Post issue Ok"),
err => window.console.log("Post issue error!")
);
}
}
So, what's going on here:
If a service worker is available, and I was able to read one, I'm saving data to the IndexedDb database.
After that, I perform a call of
sw.sync.register('sync-new-problem');
to fire synchronization event.
The 'sync' event fires and everything works perfect.
I'm catching a 'sync' event in the service worker:
self.addEventListener("sync", function(event) {
console.log('[Service Worker] background synchronization', event);
if(self.indexedDB && event.tag === 'sync-new-problem') {
console.log('[Service Worker] syncing a new Problem');
event.waitUntil(
readAllData('Problems')
.then(function(data) {
for(var dt of data) {
console.log("Data: " + dt);
})
.then(function(res) {
console.log("It's required to clean storage data: " + res);
})
.catch(error => {
console.log("Error ocurred" + error);
throw error;
})
);
}
});
When I'm registering sync task, the sync event is fired immediately and everything works well. But this event should also be automatically thrown when internet connection is restored.
I tried to turn my Wi-Fi/Ethernet Off and On. I also tried to select Offline in Chrome Development Tools. Nothing helps, 'sync' event is not fired when the network is restored.
According to Background Sync tab in Chrome, my sync task was registered. Then I turned Wi-Fi Off/On, checked and unchecked Offline checkbox on the Application tab of Chrome Dev Tools.
Nothing works, 'sync' event is not fired on connection restore.
I'd be very grateful for any suggestion, how can I get 'sync' event fired on network restore.
I tried with Chrome v.96
Tags must be unique per request. If chrome has seen the tag before (sync-new-problem) and it has been previously successful, it will consider the work done and not process it.
Add a guid to the end of the tag and I believe this will work.

Service Worker not stopping initial requests and only appears to be used once

I asked a question last week about this and got a very helpful answer but I am still struggling to get this working as it is supposed to, although at this point I'm not entirely sure what I've been asked to do is possible.
So this service worker is supposed to activate when ?testMode=true is added to the URL and this seems to be happening okay. The service worker is then supposed to intercept specific requests before they happen and then redirect it to mock data instead. What I have got at the moment will store the mock data and call it when specific requests are made but it doesn't actually stop the initial request from happening as it still appears within the network tab.
So for example if the request contains /units/all?availability=Active&roomTypeHandle=kitchens, the service worker is meant to intercept this and instead of that request going through, mock-data/unitData.json is supposed to be used instead.
This is what I have so far:
TestMode.ts
class TestMode {
constructor() {
if (!this.isEnabled()) {
return;
}
if (!('serviceWorker' in navigator)) {
console.log('Browser does not support service workers');
return;
}
this.init();
}
private init(): void {
navigator.serviceWorker
.register('planner-worker/js/worker.min.js')
.then(this.handleRegistration)
.catch((error) => { throw new Error('Service Worker registration failed: ' + error.message); });
navigator.serviceWorker.addEventListener('message', event => {
// event is a MessageEvent object
console.log(`The service worker sent me a message: ${event.data}`);
});
navigator.serviceWorker.ready.then( registration => {
if (!registration.active) {
console.log('failed to communicate')
return;
}
registration.active.postMessage("Hi service worker");
});
}
private handleRegistration(registration: ServiceWorkerRegistration): void {
registration.update();
console.log('Registration successful, scope is:', registration.scope);
}
private isEnabled(): boolean {
return locationService.hasParam('testMode');
}
}
export default new TestMode();
serviceWorker.js
const CACHE_NAME = 'mockData-cache';
const MOCK_DATA = {
'/units/all?availability=Active&roomTypeHandle=kitchens': 'mock-data/unitData.json',
'/frontal-ranges/kitchens?' : 'mock-data/kitchensData.json',
'/carcase-ranges/?availability=Active&roomTypeHandle=kitchens' : 'mock-data/carcaseRangesData.json',
'/products/830368/related?roomTypeHandle=kitchens&productStateHandle=Active&limit=1000&campaignPhaseId=183&retailStore=Finishing%20Touches%20%28Extra%29'
: 'mock-data/relatedItems.json'
};
self.addEventListener('install', event => {
console.log('Attempting to install service worker and cache static assets');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(Object.values(MOCK_DATA));
})
);
});
self.addEventListener('activate', function(event) {
return self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
const pathAndQuery = url.pathname + url.search;
if (pathAndQuery in MOCK_DATA) {
const cacheKey = MOCK_DATA[pathAndQuery];
event.respondWith(
caches.match(cacheKey, {
cacheName: CACHE_NAME,
})
);
}
});
Another thing that happens which I'm not sure how to get around is that the fetch event only happens once. By that I mean that when a change is made to serviceWorker.js, the mock data is stored in the cache and the files appear in the network tab of dev tools, but if I refresh the page, or close it and reopen in another tab, the mock data is no longer in the network tab and it's as if the service worker is not being used at all. How can I update this so that it's always used? I can see in the console log that the service worker is registered, but it just don't seem to get used.
Apologies if I shouldn't be asking 2 questions in 1 post, just really not sure how to solve my issue. Thanks in advance for any guidance.
Turns out my issue was a scope one. Once moving where the service worker was stored it started working as intended. Realised this was the case as I figured out the fetch event wasn't actually firing.

pwa - prompt add to home screen dosen't show

I develop a web application with symfony 4.3, then I add pwa features to it. I test with lighthouse extension in chrome and this is result:
Now problem is prompt for add icon to home screen dosen't show and I have this error :
Uncaught TypeError: Cannot read property 'prompt' of undefined
code js:
var deferredPrompt ;
var btnAdd = document.getElementById('butInstall') ;
function launchPromptPwa(){
var deferredPrompt;
btnAdd = document.getElementById('butInstall') ;
window.addEventListener('beforeinstallprompt', (e) => {
console.log('0');
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
btnAdd.style.display = block;
showAddToHomeScreen();
});
btnAdd.addEventListener('click', (e) => {
console.log('1');
//btnAdd.style.display = 'none';
//Show the prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
deferredPrompt.userChoice
.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
deferredPrompt = null;
});
});
window.addEventListener('appinstalled', (evt) => {
console.log('a2hs installed');
});
if (window.matchMedia('(display-mode: standalone)').matches) {
console.log('display-mode is standalone');
}
}
I test for the display prompt in chrome.
To avoid the error you can test first whether the deferredPrompt variable is initialised and skip the code logic if undefined:
if (deferredPrompt) {
deferredPrompt.prompt();
// ...
}
Then, is the beforeinstallprompt event triggered?
If so, you have to proof if the event object is defined, as you use it to initialise your variable:
deferredPrompt = e;
Keep in mind that you need a running service worker in order to let the beforeinstallprompt event being triggered. And the service worker needs a secure connection (https) or running localhost and served via web server.
You can open Chrome Dev Tools (F12) and access the "Application" tab to verify that a web manifest is correctly set and a service worker is installed.
I wrote some articles about service workers, caching strategies and PWAs if you are interested in deepening the topic.
UPDATE
If you want to serve content offline, you have to implement caching strategies for your service worker (eg. Stale while revalidate). Following the link above you can learn about different strategies and how you can implement them.
When you implement caching strategies, all the static assets (like css or js files) or data requests will be intercept by the service worker and if there is a match with the given rules, it will cache them or provide them from the cache. Since the cache is on the client side, those resources are available also offline.
As example, to cache static assets:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(
[
'/css/bootstrap.css',
'/css/main.css',
'/js/bootstrap.min.js',
'/js/jquery.min.js',
'/offline.html'
// Add anything else you need to be cached during the SW install
]
);
})
);
});

How to fallback to browser's default fetch handling within event.respondWith()?

Within the service worker my fetch handler looks like this:
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request); //<-- is this the browser's default fetch handling?
})
);
});
The method event.respondWith() forces me to handle all requests myself including xhr requests which is not what I like todo. I only want the cached resources to be returned if available and let the browser handle the rest using the browser's default fetch handling.
I have two issues with fetch(event.request):
Only when devtools is opened it produces an error while fetching the initial URL which is visible in the address bar https://test.de/x/#/page. It happens both on initial install and on every reload:
Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'ServiceWorkerGlobalScope': 'only-if-cached' can be set only with 'same-origin' mode`
and I don't understand why because I am not setting anything
It seems to violate the HTTP protocol because it tries to request a URL with an anchor inside:
Console: {"lineNumber":0, "message":"The FetchEvent for
\"https://test.de/x/#/page\" resulted in a network error
response: the promise was rejected.", "message_level":2, "sourceIdentifier":1, "sourceURL":""}`
How does fetch() differ from the browser's default fetch handling and are those differences the cause for those errors?
Additional information and code:
My application also leverages the good old appCache in parallel with the service worker (for backwards compatibility). I am not sure if the appcache interferes with the service worker installation on the initial page load. The rest of the code is pretty straight forward:
My index.html at https://test.de/x/#/page uses appcache and a base-href:
<html manifest="appcache" lang="de">
<head>
<base href="/x/"/>
</head>
...
Service Worker registration within the body script
window.addEventListener('load', {
navigator.serviceWorker.register('/x/sw.js')
});
Install and activate event
let MY_CACHE_ID = 'myCache_v1';
let urlsToCache = ['js/main.js'];
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(MY_CACHE_ID)
.then(function (cache) {
return cache.addAll(
urlsToCache.map(url => new Request(url,
{credentials:'include'}))
)
})
);
});
self.addEventListener('activate', function (event) {
//delete old caches
let cacheWhitelist = [MY_CACHE_ID];
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
fetch(event.request) should be really close to the default. (You can get the actual default by not calling respondWith() at all. It should mostly not be observable, but is with CSP and some referrer bits.)
Given that, I'm not sure how you're ending up with 1. That should not be possible. Unfortunately, you've not given enough information to debug what is going on.
As for 2, it passes the fragment on to the service worker, but that won't be included in the eventual network request. That matches how Fetch is defined and is done that way to give the service worker a bit of additional context that might be useful sometimes.

Google Cloud Messaging (GCM) not working with firefox

I have this Service Worker that receives notification well with Chrome,
but it's not receiving with firefox.
the Push listener is not fired at all in firefox (by debugging it),
PS: the service worker is successfully registered, but it's not receiving notification.
what's the problem with my code?
self.addEventListener('install', function (event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', function (event) {
console.log('Activated', event);
});
self.addEventListener('push', function (event) {
event.waitUntil(
fetch('/path', {
credentials: 'include',
method: 'post',
})
.then(function (response) {
return response.json()
.then(function (data) {
return self.registration.showNotification(data.title, {
body: data.body,
icon: '/images/image.png',
});
});
})
.catch(function (error) {
console.error('wrong', error);
})
);
});
Based from this documentation, if you are using the Channel Messaging API to comunicate with the service worker, set up a new message channel (MessageChannel.MessageChannel()) and send port2 over to the service worker by calling Worker.postMessage() on the service worker, in order to open up the communication channel. You should also set up a listener to respond to messages sent back from the service worker.
Make sure that you followed these steps on how to set up the GCM properly. You can also check this related link: GCM Equivalent for Firefox
Hope this helps!
You will need to share your code in the client for registering for push notifications.
With that said, once you register for push notifications, you will receive a subscription with an endpoint. In Firefox that endpoint will never be a GCM url, but a push server provided by Mozilla:
navigator.serviceWorker.ready
.then((reg) => reg.pushManager.subscribe({ userVisibleOnly: true }))
.then((subscription) => {
const endpoint = subscription.endpoint;
// endpoint will have different server values when using this code in Chrome or Firefox.
Here are key notes to be considered:
Are you registering for push notifications correctly in firefox?
Check the url for the endpoint you will need to reach for performing the push notification, is a mozilla server one?
Setup breakpoints just after the push listener to verify that you receive the push.

Categories

Resources