Service Worker "fetch reentrancy" - javascript

I am using the service worker to cache requests made ot a rest service.
The implementation corresponds to 'on network response' described on this page https://web.dev/offline-cookbook/.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
return (
response ||
fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
})
);
});
}),
);
});
The idea is that:
1. caller makes 1st request A
2. service worker intercepts the request A
3. service worker checks whether a request A is in storage -> no
4. service worker sends the request A to the rest service
5. service worker gets the response A
6. service worker stores the response A
7. service worker returns the response A to the caller
...
8. caller makes 2nd request A
9. service worker intercepts the request A
10. service worker checks whether a request A is in the cache -> yes
11. service worker gets the response A from storage
12. service worker returns the response A to the caller
The problem is that the rest request takes some time to return a response and this scenario where 2 or more requests are made to the rest service can occur:
1. caller makes 1st request A
2. service worker intercepts the request A
3. service worker checks whether a request A is in storage -> no
4. service worker sends the request A to the rest service
5. caller makes 2nd request A
6. service worker intercepts the request A
7. service worker checks whether a request A is in storage -> no
8. service worker gets the response A
9. service worker stores the response A
10. service worker returns the response A to the caller
11. service worker gets the response A
12. service worker stores the response A
13. service worker returns the response A to the caller
How to have only 1 request sent?

If you're open to using the Workbox libraries, instead of "vanilla" service worker code, this recipe might help:
// See https://developers.google.com/web/tools/workbox/guides/using-bundlers
import {NetworkFirst} from 'workbox-strategies';
class DedupeNetworkFirst extends NetworkFirst {
constructor(options) {
super(options);
// This maps inflight requests to response promises.
this._requests = new Map();
}
// _handle is the standard entry point for our logic.
async _handle(request, handler) {
let responsePromise = this._requests.get(request.url);
if (responsePromise) {
// If there's already an inflight request, return a copy
// of the eventual response.
const response = await responsePromise;
return response.clone();
} else {
// If there isn't already an inflight request, then use
// the _handle() method of NetworkFirst to kick one off.
responsePromise = super._handle(request, handler);
this._requests.set(request.url, responsePromise);
try {
const response = await responsePromise;
return response.clone();
} finally {
// Make sure to clean up after a batch of inflight
// requests are fulfilled!
this._requests.delete(request.url);
}
}
}
}
You could then use the DedupeNetworkFirst class along with Workbox's router, or alternatively, use it directly in your own fetch handler if you'd rather not use more of Workbox.

Related

ignore a specific endpoint in the service worker

Basically I am working with an online/offline app. I developped a custom hook to detect wether a user has connection or not. For this I am sending a random fetch request. However the service worker is intercepting the request and send a 200 even though the user is clearly offline. My question is, can I ingore a specific endpoint in the service worker ?
const checkOnline = async () => {
try {
const response = await fetch('/test.test');
setOnline(response.ok);
console.log('response.source', response.url)
} catch {
setOnline(false);
}
};
You can detect internet connection status via window.navigator.onLine. No need to make a fake request

Service Worker not receiving message

NOTE: Using Create-React-App Template... (Modifying the Service Worker in it)
I am using the communication channel api to send a message to a service worker for caching. I use an xmlhttp request because of its progress api since fetch needs an indefinite loader afaik.
So after receiving the data on readystate 4 and status code 200 I go to postMessage to SW. I get logging on client side but don't receive the message in the service worker.
I am developing locally and using a Chrome Extension to allow local testing of SW and Build:
https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb?hl=en
CLIENT SNIPPET
xhr.onreadystatechange = function(){
if (this.readyState == 4 && this.status == 200) {
const res = JSON.parse(this.responseText);
function sendMessage(msg){
console.log("SEND SW MESSAGE");
const msgChan = new MessageChannel();
// This wraps the message posting/response in a promise, which will resolve if the response doesn't
// contain an error, and reject with the error if it does. If you'd prefer, it's possible to call
// controller.postMessage() and set up the onmessage handler independently of a promise, but this is
// a convenient wrapper.
return new Promise(function(resolve, reject){
console.log("Promise Scope");
msgChan.port1.onmessage = function(event){
event.data.error ? reject(event.data.error) : resolve(event.data);
}
// This sends the message data as well as transferring messageChannel.port2 to the service worker.
// The service worker can then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
// See https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
navigator.serviceWorker.controller.postMessage(msg, [msgChan.port2]);
});
}
sendMessage(res).then(function(){
console.log("SW MESSAGE SENT");
// Storing Quake Data
that.props.setQuakes(res[0]);
// Storing Three Data Obj - SSR Parsing and Creation
that.props.setThreeData(res[1]);
// Greenlight
that.props.setVizInitSuccess(true);
}).catch(function(err){
console.log("Error Caching Data: "+err);
});
} };
SERVICE WORKER SNIPPET
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
// Set up a listener for messages posted from the service worker.
// The service worker is set to post a message to all its clients once it's run its activation
// handler and taken control of the page, so you should see this message event fire once.
// You can force it to fire again by visiting this page in an Incognito window.
navigator.serviceWorker.onmessage = function(event) {
console.log("SERVICE WORKER RECIEVED MESSAGE");
console.log(event);
event.ports[0].postMessage("SW Says Hello Back!");
if (event.data.requireData == true && 'caches' in window) {
// Check for cache'd data and load
// clients.matchAll().then(clients => {
// clients.forEach(client => {
// console.log(client);
// //send_message_to_client(client, msg).then(m => console.log("SW Received Message: "+m));
// })
// })
// caches.open('threeData').then(function(cache){
// console.log("SW Cache");
// console.log(cache)
// event.ports[0].postMessage(cache);
// });
} else {
// Cache Data
caches.open('threeData').then(function(cache){
cache.put('/data.json', new Response(event.data.json))
});
}
};
...
navigator used in Service Worker file is WorkerNavigator Interface and not Navigator Interface.
WorkerNavigator is a subset of the Navigator interface allowed to be accessed from a Worker (In this case its Service worker).
WorkerNavigator Reference.
So navigator inside Service worker file doesn't contains serviceWorker object. So it throws error when you call navigator.serviceWorker.onMessage.
To receive the messages from client use self.addEventListener in Service Worker file.
self.addEventListener('message', event => {
console.log(`[Message] event: `, event.data);
});

Get response header of cached response inside service worker

I want to get the response headers of a cached response inside a service worker. The purpose of this is so that I can read a custom header called 'Modified' to see if it is necessary to fetch a new copy of the data by comparing it to the response headers of a 'HEAD' fetch for the same URL.
On install of the service worker, I populate a cache called v1::fundamentals with some responses. I then register a fetch listener which looks for the request in the cache and if its there, serves it. I then want to async update the cache with non-stale content but only if the 'Modified' header contains a newer timestamp than the one in the cache. In the simplified code below, I try to access the headers with headers.get() but I always get a null in return. Why is this?
When I look at the cache in Chrome devtools, the headers are very much there, I just can't get to them from within the service worker.
self.addEventListener('fetch', event => {
console.log('%c[SW] Fetch caught: ', 'color: #42d9f4', event.request.url);
// Let the browser do its default thing for non-GET requests.
if (event.request.method != 'GET') {
return;
} else {
// Prevent the default, and handle the request ourselves.
event.respondWith(async function() {
// Try to get the response from a cache.
const cache = await caches.open('v1::fundamentals');
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
// Try to get the headers
var cacheDate = cachedResponse.headers.get('Modified');
// Print header, returns 'null'
console.log(cacheDate);
event.waitUntil(cache.add(event.request));
return cachedResponse;
}
return fetch(event.request);
}());
}
});

How can I make an asynchronous request (non-blocking) using a Cloudflare Worker

I'm writing a Cloudflare Worker that needs to ping an analytics service after my original request has completed. I don't want it to block the original request, as I don't want latency or a failure of the analytics system to slow down or break requests. How can I create a request which starts and ends after the original request completes?
addEventListener('fetch', event => {
event.respondWith(handle(event))
})
async function handle(event) {
const response = await fetch(event.request)
// Send async analytics request.
let promise = fetch("https://example.com")
.then(response => {
console.log("analytics sent!")
})
// If I uncomment this, only then do I see the
// "analytics sent!" log message. But I don't
// want to wait for it!
// await promise;
return response
}
You need to use Event.waitUntil() to extend the duration of the request. By default, all asynchronous tasks are canceled as soon as the final response is sent, but you can use waitUntil() to extend the request processing lifetime to accommodate asynchronous tasks. The input to waitUntil() must be a Promise which resolves when the task is done.
So, instead of:
await promise
do:
event.waitUntil(promise)
Here's the full working script in the Playground.

JS Service Worker running on pages I don't want it to run on

I just started trying to use service workers to cache files from my web server. I'm using the exact code that google suggests HERE. I uploaded the service worker script to my sites root folder and have included a link to it in the pages I want the service worker to cache files on.
I was under the impression that the service worker only caches files that are in the urlsToCache variable, and the service worker would only work on pages where the service worker script is called (linked to).
I have several pages on my site that I don't want the service worker to do anything on. However, it seems that it's still being referenced somehow. The pages in question do not contain a link to my service worker script at all. I've noticed that each time I run an AJAX command using the POST method I receive the following error in my console:
Uncaught (in promise) TypeError: Request method 'POST' is unsupported
at service-worker.js:40
at anonymous
line 40 is this snippet of code: cache.put(event.request, responseToCache);
The url my AJAX call is pointing to does not contain a link to my service worker script either.
So my question is a two parter.
1.) Does anyone understand the error message I'm receiving and know how to fix it?
2.) Why is my service worker script running on pages that don't even link to the script in the first place?
Here is the full service worker script I'm using:
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'assets/scripts/script1.js',
'assets/scripts/script2.js',
'assets/scripts/script3.js'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return 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;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
Once a service worker is installed it runs independently of your website, means all requests will go through the fetch-eventhandler, your service worker controls also the other pages.
Not only the urlsToCache are cached, the service worker also caches responses on the fly as soon as they were fetched in the fetch-eventhandler (line 38-41)
This also leads to the answer for your first question. Your code caches all responses independent of the http method, but as the error message says http POST response cannot be cached.

Categories

Resources