Get API response time using performance.now() in Service Worker - javascript

I am developing a react application and intercepting every fetch request using a service worker in place. I need to get the API response time what we normally do using performance.now() using service worker as if I am adding eventListener on its fetch method, it can intercept every fetch request.
I am not sure exactly how to achieve this and where to place performance.now() start and end calls?
Below is the code I have worked so far.
registerServiceWorker function - The function is womring fine and sw.js registers itself as service worker.
const registerServiceWorker = () => {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('Service worker registered with scope: ', registration.scope);
}, (err) => {
console.log('ServiceWorker registration failed: ', err);
});
});
} else {
console.log('Service worker is not supported!');
};
};
sw.js file - Service worker file
self.addEventListener('fetch', event => {
const apiStartTime = self.performance.now();
event.respondWith((async () => {
const { pathname, query } = new URL(event.request.url);
const apiTotalTime = Math.round(self.performance.now() - apiStartTime);
console.log('performance2 end', event.request.url, apiTotalTime);
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
const response = await fetch(event.request);
return response;
})());
});
The apiTotalTime value is coming as 0 always for every API which is not as expected.

Try placing your second call to performance.now after await fetch(event.request) - after the service call is actually made and awaited.

Related

Intercept fetch for the first time but not afterwards using serviceWorker

Need some guidance here with service worker.
When the service worker is installed, it caches the assets. On next reload, when any request is made, it is intercepted by service worker, which first checks in cache, if it isn't found, then we make a network call. But this second network call is again being intercepted by service worker and thus it has turned into an infinite loop.
I don't want the next fetch call, to be intercepted again. I hope I'm able to explain the issue here.
Here is the serviceWorker.js
const cacheVersion = "v11";
self.addEventListener('install',(event)=>{
self.skipWaiting();
event.waitUntil(caches.open(cacheVersion).then((cache)=>{
cache.addAll([
'/',
'/index.html',
'/style.css',
'/images/github.png',
])
.then(()=>console.log('cached'),(err)=>console.log(err));
}))
})
self.addEventListener('activate',event=>{
event.waitUntil(
(async ()=>{
const keys = await caches.keys();
return keys.map(async (cache)=>{
if(cache !== cacheVersion){
console.log("service worker: Removing old cache: "+cache);
return await caches.delete(cache);
}
})
})()
)
})
const cacheFirst = async (request) => {
try{
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
}
catch(err){
return fetch(request);
}
return fetch(request);
};
self.addEventListener("fetch", (event) => {
event.respondWith(cacheFirst(event.request));
});
The reason here is your cacheFirst, it's a bit wrong. What do we want to do inside it (high-level algorithm) ? Should be something like this, right?
check cache and if match found - return
otherwise, fetch from server, cache and return
otherwise, if network failed - return some "dummy" response
const cacheFirst = async (request) => {
// First try to get the resource from the cache
const responseFromCache = await caches.match(request);
if (responseFromCache) {
return responseFromCache;
}
// Next try to get the resource from the network
try {
const responseFromNetwork = await fetch(request);
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
putInCache(request, responseFromNetwork.clone());
return responseFromNetwork;
} catch (error) {
// well network failed, but we need to return something right ?
return new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
});
}
};
This is not ready-to-use solution !!! Think of it as a pseudo-code, for instance you might need to impl putInCache first.

service worker not active initially, reload needed to activate

I'm experimenting with service workers, so my scenario is very basic. The problem I'm experiencing is that when I load my page the first time, the service worker doesn't do anything (except installing) and when I reload the page, the service worker is active and intercepts.
Here is my service worker code:
self.addEventListener("install", function (event) {
// self.skipWaiting();
});
self.addEventListener("fetch", function (event) {
const url = event.request.url;
if (url.includes("todos")) {
const response = {
body: { data: "barfoo" },
init: {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/json",
"X-Mock-Response": "yes",
},
},
};
const mockResponse = new Response(JSON.stringify(response.body),
response.init);
event.respondWith(mockResponse);
}
});
If the URL contains todo it sends a mock response back.
The last piece is my index.html:
<html>
<body>
<h1>Test page with service worker</h1>
<button>Fetch</button>
<script>
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register(
"/service-worker.js",
// {
// scope: "/sw-test/",
// }
);
if (registration.installing) {
console.log("Service worker installing");
} else if (registration.waiting) {
console.log("Service worker installed");
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Registration failed with ${error}`);
}
}
};
document
.querySelector("button")
.addEventListener("click", async (e) => {
const output = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const json = await output.json();
console.log("OUTPUT", json);
});
registerServiceWorker();
</script>
In the html (all is served from http://localhost:8080 btw) is a button which triggers the fetch call on click.
As I said before, the first time I get Service worker installing in the console. Then, when I hit the button I receive the response from jsonplaceholder (self.skipWaiting() didn't help). I can hit the button many times with the same result. Now, when I hit reload I get Service worker active in the console and everything is now working as expected (I get the mock response)
So my question is, what do I have to do to get mock responses initially so the reload is not needed?

Service worker offline page won't load

This used to work for me but stopped a couple of months ago and I've tinkered my way right out of being able to figure this out anymore. What am I doing wrong here?
Call the service worker template, no problem:
if(navigator.serviceWorker){
window.addEventListener('load',() => {
navigator.serviceWorker
.register('/sw.js')
.then(console.log('[ServiceWorker] Registered Successfully'))
.catch(err => console.log(`[ServiceWorker] Error: ${err}`));
});
} else {
console.log('Service Worker not supported.');
}
Setup a cache version and preloaded the cache, no problem:
const cacheName='2020.10.06-01';
var cacheFiles = ['/offline.html'];
Installed the Services Worker, no problem:
addEventListener('install', e => {
e.waitUntil(
caches.open(cacheName).then(cache => {
return cache.addAll(cacheFiles);
})
);
});
Activated the Services Worker for auto cache rollover, no problem:
addEventListener('activate', e => {
e.waitUntil(
caches.keys().then(keyList => {
return Promise.all(keyList.map(key => {
if(key !== cacheName) {
return caches.delete(key);
}
}));
})
);
});
Fetching from cache or network, no problem:
addEventListener('fetch', e => {
e.respondWith(async function() {
try {
const cache = await caches.open(cacheName);
const cachedResponse = await cache.match(e.request);
const networkResponsePromise = fetch(e.request);
e.waitUntil(async function() {
const networkResponse = await networkResponsePromise;
await cache.put(e.request, networkResponse.clone());
}());
// Returned the cached response if we have one, otherwise return the network response.
return cachedResponse || networkResponsePromise;
} catch (error) {
console.log('Fetch failed; returning offline page instead.', error);
const cache = await caches.open(cacheName);
const cachedResponse = await cache.match('/offline.html');
return cachedResponse;
}
}());
});
But if the page/resource I'm trying to request is not already in the cache AND the network is not available it refuses to display my 'offline.html' page. (Which I know IS in the cache)
Any ideas?
Here's the Fetch code I wrote in the end that works perfectly for me:
self.addEventListener('fetch', (event) => {
event.respondWith((async() => {
const cache = await caches.open(cacheName);
try {
const cachedResponse = await cache.match(event.request);
if(cachedResponse) {
console.log('cachedResponse: ', event.request.url);
return cachedResponse;
}
const fetchResponse = await fetch(event.request);
if(fetchResponse) {
console.log('fetchResponse: ', event.request.url);
await cache.put(event.request, fetchResponse.clone());
return fetchResponse;
}
} catch (error) {
console.log('Fetch failed: ', error);
const cachedResponse = await cache.match('/en/offline.html');
return cachedResponse;
}
})());
});
This does everything I need, in a very specific order. It checks the cache first, if found it's returned. It checks the network next, if found it caches it first then returns it. Or it displays a custom offline page with a big Reload button to encourage visitors to try again when they are back online.
But the most important this to realise is that doing it this way alows me to display a page and all it's resources with or without network access.
UPDATE: In order to deal with changes to CORS security requirements that where implemented in all browsers between March and August of 2020, I had to make one small change to the 'fetch' event.
Changed from:
const fetchResponse = await fetch(event.request);
To:
const fetchResponse = await fetch(event.request, {mode:'no-cors'});
Replace your fetch event code with this one. For every request your fetch event will be invoked and it will check if your request is found in the cache file list then it will serve the file from there otherwise it will make the fetch call to get the file from server.
self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
Also you don't need a separate "offline.html" file in your cache file list. Instead add your main application html file and your relevant css and js files in that list. That will make your application completely offline in case of no network.

registration.showNotification is not a function

I'm using serviceworker-webpack-plugin to create a service worker in my reactjs apps.
I've followed the example to register the service worker in the main thread. I've learnt that Html5 Notification doesn't work on Android chrome, so I used registration.showNotification('Title', { body: 'Body.'}); instead of new Notification('...') to push notifications. But when I tested it on the desktop chrome, it throws this error
registration.showNotification is not a function
Is the registration.showNotification only available on Android chrome but not on the desktop?
public componentDidMount(){
if ('serviceWorker' in navigator &&
(window.location.protocol === 'https:' || window.location.hostname === 'localhost')
) {
const registration = runtime.register();
registerEvents(registration, {
onInstalled: () => {
registration.showNotification('Title', { body: 'Body.'});
}
})
} else {
console.log('serviceWorker not available')
}
}
runtime.register() returns a JavaScript Promise, which is why you are getting a not a function error because Promises don't have a showNotification() method.
Instead, you'd have to chain a .then() callback to it in order to get the actual registration object (or use async/await, which is also cool).
runtime.register().then(registration => {
registration.showNotification(...);
})
Below solution worked for me. Can try.
The main root cause of .showNotification() not firing is service worker. Service worker is not getting registered. So it wont call registration.showNotification() method.
Add service-worker.js file to your project root directory
You can download service-worker.js file from Link
Use below code to register service worker and fire registration.showNotification() method.
const messaging = firebase.messaging();
messaging.onMessage(function (payload) {
console.log("Message received. ", payload);
NotisElem.innerHTML = NotisElem.innerHTML + JSON.stringify(payload);
//foreground notifications
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./service-worker.js', { scope: './' })
.then(function (registration) {
console.log("Service Worker Registered");
setTimeout(() => {
registration.showNotification(payload.data.title, {
body: payload.data.body,
data: payload.data.link
});
registration.update();
}, 100);
})
.catch(function (err) {
console.log("Service Worker Failed to Register", err);
})
}
});

Updating Web App User Interface when application is in background FCM

Am using FCM to handle notifications, it works fine up until when I need to update my UI from the firebase-messaging-sw.js when my web app is in the background.
My first question is: is it possible to update my web app UI in the background (When user is not focused on the web app) through a service worker
Secondly, if so, how? because I tried a couple of things and its not working, obviously am doing something wrong and when it does work, my web app is in the foreground. What am I doing wrong?
My codes are below.
my-firebase-service-sw.js
// [START initialize_firebase_in_sw]
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase
libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/4.1.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.1.1/firebase-messaging.js');
// My Custom Service Worker Codes
var CACHE_NAME = 'assembly-v0.1.3.1';
var urlsToCache = [
'/',
'lib/vendors/bower_components/animate.css/animate.min.css',
'lib/vendors/bower_components/sweetalert/dist/sweetalert.css',
'lib/css/app_1.min.css',
'lib/css/app_2.min.css',
'lib/css/design.css'
];
var myserviceWorker;
var servicePort;
// Install Service Worker
self.addEventListener('install', function (event) {
console.log('installing...');
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
console.log('installed...');
});
// Service Worker Active
self.addEventListener('activate', function (event) {
console.log('activated!');
// here you can run cache management
var cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
// Cache hit - return response
if (response) {
return response;
}
// IMPORTANT: Clone the request. A request is a stream and
// can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need
// to clone the response.
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function (response) {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function (cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
self.addEventListener('message', function (event) {
console.log("SW Received Message: " + event.data);
// servicePort = event;
event.ports[0].postMessage("SW Replying Test Testing 4567!");
});
myserviceWorker = self;
// Initialize the Firebase app in the service worker by passing in the
// messagingSenderId.
firebase.initializeApp({
'messagingSenderId': '393093818386'
});
// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();
// [END initialize_firebase_in_sw]
// If you would like to customize notifications that are received in the
// background (Web app is closed or not in browser focus) then you should
// implement this optional method.
// [START background_handler]
messaging.setBackgroundMessageHandler(function (payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
// Customize notification here
// send to client
console.log('Sending data to notification');
try {
myserviceWorker.clients.matchAll().then(function (clients) {
clients.forEach(function (client) {
console.log('sending to client ' + client);
client.postMessage({
"msg": "401",
"dta": payload.data
});
})
});
} catch (e) {
console.log(e);
}
const notificationTitle = payload.data.title;;
const notificationOptions = {
body: payload.data.body,
icon: payload.data.icon,
click_action: "value"
};
return self.registration.showNotification(notificationTitle,
notificationOptions);
});
// [END background_handler]
In my main javascript file, which receives the payload. it receives it when the application is in the foreground. My major concern and problem is receiving payload when the application is in the background, all activities on foreground works just fine.
It is possible to update the UI even your website is opening but unfocused.
Just add enable option includeUncontrolled when you get all client list.
Example:
messaging.setBackgroundMessageHandler(function (payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
self.clients.matchAll({includeUncontrolled: true}).then(function (clients) {
console.log(clients);
//you can see your main window client in this list.
clients.forEach(function(client) {
client.postMessage('YOUR_MESSAGE_HERE');
})
})
});
In your main page, just add listener for message from service worker.
Ex:
navigator.serviceWorker.addEventListener('message', function (event) {
console.log('event listener', event);
});
See Clients.matchAll() for more details.

Categories

Resources