Service worker: Cache by filename only not url - service-worker

I noticed that when you cache a file with a service worker
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
the file is cached based on the url
/bundle/v1/assets/icon.svg
But what I would like to have is only the file name on which the cache is base. Meaning that the same cache is used for
/bundle/v1/assets/icon.svg
/bundle/v2/assets/icon.svg
/bundle/v3/assets/icon.svg
Is something like this possible?

What you can do is get a bit creative when you read and write to the Cache Storage API, by normalizing the URLs prior to using them as keys.
To use your example, let's say you know that every time you interact with the Cache Storage API (read or write) for a URL ending in icon.svg, you always expect it to refer to the same underlying Response, regardless of the full URL. You could do something like the following:
// A "fake" prefix used for all the normalized entries.
// Choose something that is not used by your real URLs.
const NORMALIZED_PREFIX = '/__normalized/';
// A list of "files" whose URLs you want to normalize.
const FILES_TO_NORMALIZE = [
'icon.svg',
// ...anything else...
];
function normalizeUrl(request) {
// Convert the request.url string into a URL object,
// so that we can get the pathname cleanly.
const url = new URL(request.url);
for (const file of FILES_TO_NORMALIZE) {
if (pathname.endsWith(file)) {
// If we have a match, then normalize the URL by using our
// standard prefix along with the specific file name.
url.pathname = NORMALIZED_PREFIX + file;
return url.href;
}
}
// Otherwise, just return the original request's URL.
return request.url;
}
self.addEventListener('fetch', event => {
// This is a naive cache-first strategy. Customize to suit your needs:
// https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/
event.respondWith(async function() {
const requestKey = normalizeRequestUrl(event.request);
const cache = await caches.open('runtime-cache');
const cachedResponse = await caches.match(requestKey);
// If there's a cached response, use it.
if (cachedResponse) {
return cachedResponse;
}
// Otherwise, get a response from the network for the original request.
// Make sure you *don't* call fetch(requestKey),
// since that URL doesn't exist on the server!
const networkResponse = await fetch(event.request);
// When you save the entry in the cache, use cache.put()
// with requestKey as the first parameter.
await cache.put(requestKey, networkResponse.clone());
return networkResponse;
}());
});

Related

How to retrieve JSON object stored in cache from service worker?

I have a Json object stored in cache , Please see my cache here.
And I want to retrieve the json values from my service worker
caches.open('my-post-request').then(function (cache) {
cache.match('/cached-products.json').then(function (matchedResponse) {
return fetch('/cached-products.json').then(function (response) {
return response;
})
});
});
is there a way to do that? exploring the response in the console I can just see the properties headers, ok, status, type, url, body, but I cant find my json values anywhere.
I would appreciate any suggestion.
Thanks
You could try something like this:
var CACHE_NAME = 'dependencies-cache';
self.addEventListener('install', function(event) {
console.log('[install] Kicking off service worker registration!');
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) { // With the cache opened, load a JSON file containing an array of files to be cached
return fetch('files-to-cache.json').then(function(response) {
return response.json(); // Once the contents are loaded, convert the raw text to a JavaScript object
}).then(function(files) {
console.log('[install] Adding files from JSON file: ', files); // this will log the cached json file
return cache.addAll(files); // Use cache.addAll just as you would a hardcoded array of items
});
})
.then(function() {
console.log(
'[install] All required resources have been cached;',
'the Service Worker was successfully installed!'
);
return self.skipWaiting(); // Force activation
})
);
});
This will solve your problem.
From the code above, you can simply return your response as response.json() to convert the raw text to a Javascript Object. For full implementation of Service Worker to cache JSON file, you can visit this documentation.

Configure Workbox to use cached response when network response is a 404

This question was originally asked in a tweet.
Is there a way to configure Workbox to respond with a cached response when a network response has an HTTP status of 404?
Yes, you can create your own custom handlerCallback that accomplishes that. Some of the details will vary based on your specific setup (the cache names, the fallback URLs, etc.), as well as whether you want to use a formal Workbox strategy (like networkFirst instead of fetch()), but in general, the following should work:
// Assume that this URL is already cached somewhere, e.g. precached.
const fallbackUrl = '/404-fallback.html';
const notFoundFallbackHandler = async ({event}) => {
const fetchResponse = await fetch(event.request);
if (fetchResponse.status === 404) {
return caches.match(fallbackUrl);
} else {
return fetchResponse;
}
};
// To apply this handler based on a URL pattern:
workbox.routing.registerRoute(
new RegExp('/some/criteria/to/match'),
notFoundFallbackHandler
);
// Or, to apply this handler for all navigation requests, use this:
// const navigationRoute = new workbox.routing.NavigationRoute(notFoundFallbackHandler);
// workbox.routing.registerRoute(navigationRoute);

Falcor Router should return the value from external API

I am new to JavaScript frameworks and currently trying to setup a falcor router calling an external api (for now consider it as an express api app + mango db, hosted at 3000 port).
Now, I am able to use the request package (commented out lines) and successfully call the Express Api app (which returns obj.rating = 4). But I am unable to send this value from the falcor router instead of the hard-coded value "5".
Below is the falcor-router's server.js code:
app.use('/rating.json', falcorExpress.dataSourceRoute(function (req, res) {
return new Router([
{
route: "rating",
get: function() {
var obj;
// request('http://localhost:3000/rating/101', function (error, response, body) {
// obj = JSON.parse(body);
// console.log('rating:', obj.rating); // obj.rating = 4
// });
return {path:["rating"], value:"5"};
}
}
]);
}));
The below is the code for index.html:
<script>
function showRating() {
var model = new falcor.Model({source: new falcor.HttpDataSource('http://localhost/rating.json') });
model.
get("rating").
then(function(response) {
document.getElementById('filmRating').innerText = JSON.stringify(response.json,null, 4);
});
}
</script>
I also tried to look at the global variable declaration, synchronize http request calls, promises, then statements etc. But nothing seemed to work, clearly I am missing out something here - not sure what.
The router's get handler expects the return value to be a promise or an observable that resolves to a pathValue. To get your request against the db to work, simply return a promise that resolves to a pathValue, e.g.
return new Router([
{
route: "rating",
get: function() {
return request('http://localhost:3000/rating/101', function (error, response, body) {
return { path: ["rating", value: JSON.parse(body).rating };
});
}
}
]);

Pass custom data to service worker sync?

I need to make a POST request and send some data. I'm using the service worker sync to handle offline situation.
But is there a way to pass the POST data to the service worker, so it makes the same request again?
Cause apparently the current solution is to store requests in some client side storage and after client gets connection - get the requests info from the storage and then send them.
Any more elegant way?
PS: I thought about just making the service worker send message to the application code so it does the request again ... but unfortunately it doesn't know the exact client that registered the service worker :(
You can use fetch-sync
or i use postmessage to fix this problem, which i agree that indexedDB looks trouble.
first of all, i send the message from html.
// send message to serviceWorker
function sync (url, options) {
navigator.serviceWorker.controller.postMessage({type: 'sync', url, options})
}
i got this message in serviceworker, and then i store it.
const syncStore = {}
self.addEventListener('message', event => {
if(event.data.type === 'sync') {
// get a unique id to save the data
const id = uuid()
syncStore[id] = event.data
// register a sync and pass the id as tag for it to get the data
self.registration.sync.register(id)
}
console.log(event.data)
})
in the sync event, i got the data and fetch
self.addEventListener('sync', event => {
// get the data by tag
const {url, options} = syncStore[event.tag]
event.waitUntil(fetch(url, options))
})
it works well in my test, what's more you can delete the memory store after the fetch
what's more, you may want to send back the result to the page. i will do this in the same way by postmessage.
as now i have to communicate between each other, i will change the fucnction sync into this way
// use messagechannel to communicate
sendMessageToSw (msg) {
return new Promise((resolve, reject) => {
// Create a Message Channel
const msg_chan = new MessageChannel()
// Handler for recieving message reply from service worker
msg_chan.port1.onmessage = event => {
if(event.data.error) {
reject(event.data.error)
} else {
resolve(event.data)
}
}
navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2])
})
}
// send message to serviceWorker
// you can see that i add a parse argument
// this is use to tell the serviceworker how to parse our data
function sync (url, options, parse) {
return sendMessageToSw({type: 'sync', url, options, parse})
}
i also have to change the message event, so that i can pass the port to sync event
self.addEventListener('message', event => {
if(isObject(event.data)) {
if(event.data.type === 'sync') {
// in this way, you can decide your tag
const id = event.data.id || uuid()
// pass the port into the memory stor
syncStore[id] = Object.assign({port: event.ports[0]}, event.data)
self.registration.sync.register(id)
}
}
})
up to now, we can handle the sync event
self.addEventListener('sync', event => {
const {url, options, port, parse} = syncStore[event.tag] || {}
// delete the memory
delete syncStore[event.tag]
event.waitUntil(fetch(url, options)
.then(response => {
// clone response because it will fail to parse if it parse again
const copy = response.clone()
if(response.ok) {
// parse it as you like
copy[parse]()
.then(data => {
// when success postmessage back
port.postMessage(data)
})
} else {
port.postMessage({error: response.status})
}
})
.catch(error => {
port.postMessage({error: error.message})
})
)
})
At the end. you cannot use postmessage to send response directly.Because it's illegal.So you need to parse it, such as text, json, blob, etc. i think that's enough.
As you have mention that, you may want to open the window.
i advice that you can use serviceworker to send a notification.
self.addEventListener('push', function (event) {
const title = 'i am a fucking test'
const options = {
body: 'Yay it works.',
}
event.waitUntil(self.registration.showNotification(title, options))
})
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(
clients.openWindow('https://yoursite.com')
)
})
when the client click we can open the window.
To comunicate with the serviceworker I use a trick:
in the fetch eventlistener I put this:
self.addEventListener('fetch', event => {
if (event.request.url.includes("sw_messages.js")) {
var zib = "some data";
event.respondWith(new Response("window.msg=" + JSON.stringify(zib) + ";", {
headers: {
'Content-Type': 'application/javascript'
}
}));
}
return;
});
then, in the main html I just add:
<script src="sw_messages.js"></script>
as the page loads, global variable msg will contain (in this example) "some data".

How can I upload a PDF using Dart's HttpClient?

I need to post a PDF file to a remote REST API, and I can't for the life of me figure it out. No matter what I do, the server responds that I have not yet associated an object with the file parameter. Let's say that I have a PDF called test.pdf. This is what I've been doing so far:
// Using an HttpClientRequest named req
req.headers.contentType = new ContentType('application', 'x-www-form-urlencoded');
StringBuffer sb = new StringBuffer();
String fileData = new File('Test.pdf').readAsStringSync();
sb.write('file=$fileData');
req.write(sb.toString());
return req.close();
Thus far, I've tried virtually every combination and encoding of the data that I write() to the request, but to no avail. I've tried sending it as codeUnits, I've tried encoding it using a UTF8.encode, I've tried encoding it using a Latin1Codec, everything. I'm stumped.
Any help would be greatly appreciated.
You can use MultipartRequest from the http package :
var uri = Uri.parse("http://pub.dartlang.org/packages/create");
var request = new http.MultipartRequest("POST", url);
request.fields['user'] = 'john#doe.com';
request.files.add(new http.MultipartFile.fromFile(
'package',
new File('build/package.tar.gz'),
contentType: new ContentType('application', 'x-tar'));
request.send().then((response) {
if (response.statusCode == 200) print("Uploaded!");
});
Try using the multipart/form-data header rather than x-www-form-urlencoded. This should be used for binary data, also can you show your full req request?
void uploadFile(File file) async {
// string to uri
var uri = Uri.parse("enter here upload URL");
// create multipart request
var request = new http.MultipartRequest("POST", uri);
// if you need more parameters to parse, add those like this. i added "user_id". here this "user_id" is a key of the API request
request.fields["user_id"] = "text";
// multipart that takes file.. here this "idDocumentOne_1" is a key of the API request
MultipartFile multipartFile = await http.MultipartFile.fromPath(
'idDocumentOne_1',
file.path
);
// add file to multipart
request.files.add(multipartFile);
// send request to upload file
await request.send().then((response) async {
// listen for response
response.stream.transform(utf8.decoder).listen((value) {
print(value);
});
}).catchError((e) {
print(e);
});
}
I used file picker to pick file.
Here is the codes for pick file.
Future getPdfAndUpload(int position) async {
File file = await FilePicker.getFile(
type: FileType.custom,
allowedExtensions: ['pdf','docx'],
);
if(file != null) {
setState(() {
file1 = file; //file1 is a global variable which i created
});
}
}
here file_picker flutter library.

Resources