How to tell Service Worker which files to cache - google-chrome

What I’m doing
I’m building a Service Worker component. I want it to have:
A single worker.js file containing the Service Worker implementation.
I want to be able to tell the worker which files to cache, as well as the name of the cache.
Why? I want to require this module in several projects and I don’t want any of them to modify the worker file. The worker should be able to receive a list of paths to cache.
What I’ve tried
I tried passing a configuration object to the register method but it didn’t work. The worker didn't receive the object.
Taking advantage of the postMessage API which is available on the worker I did this:
In the registration of the worker, I’ve sent a message to the worker containing an object with the routes to cache.
Inside the worker, I’ve subscribed the worker to this message and used the paths to create the cache.
See it for yourself
Registration of the worker in the main thread
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/worker.js').then(function(reg) {
navigator.serviceWorker.controller.postMessage({
'hello': 'world',
cacheName: 'v1',
urlsToCache: [
"/index.html"
]
});
}, function(err) {
console.log('ಠ_ಠ Nope.', err);
});
}
The worker file
'use strict';
var cacheName,
urlsToCache;
importScripts('/node_modules/serviceworker-cache-polyfill/index.js');
self.addEventListener('message', function (evt) {
cacheName = evt.data.cacheName;
urlsToCache = evt.data.urlsToCache;
});
self.addEventListener('install', function(event) {
setTimeout(function(){
event.waitUntil(
caches.open(cacheName)
.then(function(cache) {
console.log('Opened cache:', cache);
return cache.addAll(urlsToCache);
})
);
}, 2000);
});
What is wrong with this?
I had to delay the opening of the cache by using a setTimeout, which is wrong, ugly and unreliable.
What are you trying to achieve, man? ಠ_ಠ
I want to find a way to tell the worker to wait until the message containing the paths to cache arrives.
Link to my repo
Thanks in advance.

I've sent you a Pull Request: https://github.com/cristianelias/serviceworker_component/pull/1. Basically what I did is use the cache object from the page itself as follow:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/worker.js').then(function(reg) {
caches.open('pages').then(function(pages){
return pages.add('test.html');
}).then(function(){
console.log('cached!');
});
}, function(err) {
console.log('ಠ_ಠ Nope.', err);
});
}
And instructed the SW to only perform a cache match:
'use strict';
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(match){
return match || fetch(event.request);
})
);
});
It works as expected on Chrome 45, I don't know on older versions since moving the cache object to the page is quite a new thing.

Related

PWA: Chrome warning "Service worker does not have the 'fetch' handler"

I'm currently unsuccessfully trying to make my PWA installable. I have registered a SertviceWorker and linked a manifest as well as I am listening on the beforeInstallPromt event.
My ServiceWorker is listening to any fetch event.
My problem is, that the created beforeInstall banner is just being shown on Chrome desktop but on mobile I get a warning in Chrome inspection tab "Application" in the "Manifest" section:
Installability
Service worker does not have the 'fetch' handler
You can check the message on https://dev.testapp.ga/
window.addEventListener('beforeinstallprompt', (e) => {
// Stash the event so it can be triggered later.
deferredPrompt = e;
mtShowInstallButton();
});
manifest.json
{"name":"TestApp","short_name":"TestApp","start_url":"https://testapp.ga/loginCheck","icons":[{"src":"https://testapp.ga/assets/icons/launcher-ldpi.png","sizes":"36x36","density":0.75},{"src":"https://testapp.ga/assets/icons/launcher-mdpi.png","sizes":"48x48","density":1},{"src":"https://testapp.ga/assets/icons/launcher-hdpi.png","sizes":"72x72","density":1.5},{"src":"https://testapp.ga/assets/icons/launcher-xhdpi.png","sizes":"96x96","density":2},{"src":"https://testapp.ga/assets/icons/launcher-xxhdpi.png","sizes":"144x144","density":3},{"src":"https://testapp.ga/assets/icons/launcher-xxxhdpi.png","sizes":"192x192","density":4},{"src":"https://testapp.ga/assets/icons/launcher-web.png","sizes":"512x512","density":10}],"display":"standalone","background_color":"#ffffff","theme_color":"#0288d1","orientation":"any"}
ServiceWorker:
//This array should NEVER contain any file which doesn't exist. Otherwise no single file can be cached.
var preCache=[
'/favicon.png',
'/favicon.ico',
'/assets/Bears/bear-standard.png',
'/assets/jsInclude/mathjax.js',
'/material.js',
'/main.js',
'functions.js',
'/material.css',
'/materialcolors.css',
'/user.css',
'/translations.json',
'/roboto.css',
'/sw.js',
'/'
];
//Please specify the version off your App. For every new version, any files are being refreched.
var appVersion="v0.2.1";
//Please specify all files which sould never be cached
var noCache=[
'/api/'
];
//On installation of app, all files from preCache are being stored automatically.
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(appVersion+'-offline').then(function(cache) {
return cache.addAll(preCache).then(function(){
console.log('mtSW: Given files were successfully pre-cached')
});
})
);
});
function shouldCache(url) {
//Checking if url is market as noCache
var isNoCache=noCache.includes(url.substr(8).substr(url.substr(8).indexOf("/")))||noCache.includes((url.substr(8).substr(url.substr(8).indexOf("/"))).substr(0,(url.substr(8).substr(url.substr(8).indexOf("/"))).indexOf("?")));
//Checking of hostname of request != current hostname
var isOtherHost=url.substr(8).substr(0,url.substr(8).indexOf("/"))!=location.hostname&&url.substr(7).substr(0,url.substr(7).indexOf("/"))!=location.hostname;
return((url.substr(0,4)=="http"||url.substr(0,3)=="ftp") && isNoCache==false && isOtherHost==false);
}
//If any fetch fails, it will look for the request in the cache and serve it from there first
self.addEventListener('fetch', function(event) {
//Trying to answer with "online" version if fails, using cache.
event.respondWith(
fetch(event.request).then(function (response) {
if(shouldCache(response.url)) {
console.log('mtSW: Adding file to cache: '+response.url);
caches.open(appVersion+'-offline').then(function(cache) {
cache.add(new Request(response.url));
});
}
return(response);
}).catch(function(error) {
console.log( 'mtSW: Error fetching. Serving content from cache: ' + error );
//Check to see if you have it in the cache
//Return response
//If not in the cache, then return error page
return caches.open(appVersion+'-offline').then(function (cache) {
return cache.match(event.request).then(function (matching) {
var report = !matching || matching.status == 404?Promise.reject('no-match'): matching;
return report
});
});
})
);
})
I checked the mtShowInstallButton function. It's fully working on desktop.
What does this mean? On the Desktop, I never got this warning, just when using a handheld device/emulator.
Fetch function is used to fetch JSon manifest file. Try reading google docs again.
For adding PWA in Mobile you need manifest file to be fetched which is fetched using service-worker using fetch function.
Here is the code :
fetch('examples/example.json')
.then(function(response) {
// Do stuff with the response
})
.catch(function(error) {
console.log('Looks like there was a problem: \n', error);
});
for more about fetch and manifest try this.

Service-Worker, "TypeError:Request failed at <anonymous>"

I hope you can help me with my problem.
Currently I build a PWA with a service-worker. It registerd successful, but something is wrong with the installation.
The "caches.open"-promise result in an error: "TypeError: Request failed at ". You can see in Chrome, that the cache is registerd, but empty.
I already checked the cache urls thousand times..
Here is my Service-worker Code
var CACHE_NAME = 'surv-cache-1';
var resourcesToCache = [
'/',
'/index.html',
'/jquery-3.2.1.min.js',
'/pouchdb.min-6.4.1.js',
'/styles/inline.css',
'/scripts/app.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(
// open the app browser cache
caches.open(CACHE_NAME).then(function(cache) {
console.log("Install succesfull");
// add all app assets to the cache
return cache.addAll(resourcesToCache);
}).then(function(out){
console.log(out);
}).catch(function(err){
console.log(err);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
// try to find corresponding response in the cache
caches.match(event.request)
.then(function(response) {
if (response) {
// cache hit: return cached result
return response;
}
// not found: fetch resource from the server
return fetch(event.request);
}).catch(function(err){
console.log(err);
})
);
});
And my registration code:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js').then(function(registration) {
console.log('Service worker registered:'+registration.scope);
}).catch(function(e) {
console.log(e);
});
};
I didn't get it.. I hope you have an idea :)
EDIT: I think I know now why it don't work. I have a authentication for my domain, so not everybody can access it.
While my serviceworker want to caching the data, it get 401 back. So it seems to be a problem with the authentication.
Maybe someone had already the same problem?
This happens when your resourcesToCache includes something that returns a 404 response. Make sure you have everything typed correctly. Also make sure that the scope is correct. You can check your worker scope using:
if("serviceWorker" in navigator) {
navigator.serviceWorker
.register(`worker.js`)
.then(registration => {
console.log("SW scope:", registration.scope);
});
}
If your project is not in your server domain root, doing something like this might help:
//your main js
if("serviceWorker" in navigator) {
navigator.serviceWorker
.register(`${window.location.pathname}worker.js`)
.then(registration => {
//do your thing
});
}
//your worker js
let resourcesToCache = [
'./',
'./index.html',
'./jquery-3.2.1.min.js',
'./pouchdb.min-6.4.1.js',
'./styles/inline.css',
'./scripts/app.js',
];
//...
As a side-note, you should be loading your libraries (jQuery, pouchdb), from a CDN to improve performance. Those can be cached too:
let resourcesToCache = [
'./',
'./index.html',
'https://code.jquery.com/jquery-3.3.1.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/pouchdb/6.4.3/pouchdb.min.js',
'./styles/inline.css',
'./scripts/app.js',
];
This happened to me when I was developing locally on a windows machine and deploying on a linux server, the problem is with the path. You need to add a '.' before your path for it to be like "./" as follows:
var resourcesToCache = [
'./',
'./index.html',
'./jquery-3.2.1.min.js',
'./pouchdb.min-6.4.1.js',
'./styles/inline.css',
'./scripts/app.js'
];
This had happened to me when i was referring a file (that don't exist in the path specified) for caching. When i updated the path to the file, things got fine.

Cancel promises in Promise.map if one is rejected (Bluebird 3)

I use Bluebird 3 with enabled cancellation. Is cancellation the tool to use in the following use case:
var resourcesPromise = Promise.map(resourceIds, function(id) {
return loadResource(id);
});
resourcesPromise.catch(function() {
resourcesPromise.cancel();
});`
If one of the resources fails to load, resourcesPromise will be rejected, and I want to stop the loading of all other resources. But as far as I can tell, cancelling resourcesPromise doesn't work, because it is already rejected.
Edit: I'm currently considering variants of the following:
var resourcesPromise = new Promise(function(resolve, reject) {
var intermediatePromise = Promise.map(resourceIds, function(id) {
return loadResource(id).catch(function(error) {
intermediatePromise.cancel();
reject(error);
});
}).then(resolve, reject);
});
(I may have found a legitimate use for the ".then(resolve, reject)" anti-pattern!)
Any ideas why Promise.map doesn't work like that?
With map you are parallelizing the resource loading, maybe is better to use Promise.each to laoad resources in sequence. In such case you don't need to cancel the promise to stop loading the remaining resources when one fails.
var resourcesPromise = Promise.map(resourceIds, function(id) {
return loadResource(id);
});
resourcesPromise.catch(function() {
resourcesPromise.cancel();
});`
Another option would be to pass to the map function an option object like in which you can specify the concurrency limit.
Promise.resolve(resourceIds).
map(function(id) {
return loadResource(id);
}, {concurrency: n}).
catch(function(e) {
//do some error handling
});

Service retrieves data from datastore but does not update ui

I have a service which retrieves data from the datastore (Web SQL). Afterwards, it stores the data in a AngularJS array. The problem is that this does not initiate changes to the UI.
Contrary, if after the retrieval of data from datastore, I call a web services using a $get method and append the results to the previous array, all data updates the UI.
Any suggestions? Is it possible that I fill the array before the Angular binds the variable?
Can I somehow delay the execution of the service?
Most of the code has been taken from the following example: http://vojtajina.github.io/WebApp-CodeLab/FinalProject/
In order for the UI to magically update, some changes must happen on properties of the $scope. For example, if retrieving some users from a rest resource, I might do something like this:
app.controller("UserCtrl", function($http) {
$http.get("users").success(function(data) {
$scope.users = data; // update $scope.users IN the callback
}
)
Though there is a better way to retrieve data before a template is loaded (via routes/ng-view):
app.config(function($routeProvider, userFactory) {
$routeProvider
.when("/users", {
templateUrl: "pages/user.html",
controller: "UserCtrl",
resolve: {
// users will be available on UserCtrl (inject it)
users: userFactory.getUsers() // returns a promise which must be resolved before $routeChangeSuccess
}
}
});
app.factory("userFactory", function($http, $q) {
var factory = {};
factory.getUsers = function() {
var delay = $q.defer(); // promise
$http.get("/users").success(function(data){
delay.resolve(data); // return an array of users as resolved object (parsed from JSON)
}).error(function() {
delay.reject("Unable to fetch users");
});
return delay.promise; // route will not succeed unless resolved
return factory;
});
app.controller("UserCtrl", function($http, users) { // resolved users injected
// nothing else needed, just use users it in your template - your good to go!
)
I have implemented both methods and the latter is far desirable for two reasons:
It doesn't load the page until the resource is resolved. This allows you to place a loading icon, etc, by attaching handlers on the $routeChangeStart and $routeChangeSuccess.
Furthermore, it plays better with 'enter' animations in that, all your items don't annoyingly play the enter animation every time the page is loaded (since $scope.users is pre populated as opposed to being updated in a callback once the page has loaded).
Assuming you're assigning the data to the array in the controller, set an $scope.$apply() after to have the UI update.
Ex:
$scope.portfolio = {};
$scope.getPortfolio = function() {
$.ajax({
url: 'http://website.com:1337/portfolio',
type:'GET',
success: function(data, textStatus, jqXHR) {
$scope.portfolio = data;
$scope.$apply();
},
error: function(jqXHR, textStatus, errorThrown) {
console.log(errorThrown);
}
});
};

HTML5 Web Workers in NodeJS?

Anyone knows what the status of Web Worker support in NodeJS is? I found a two year old implementation, node-webworkers, but it didn't run with the current build of NodeJS.
Now there is https://github.com/audreyt/node-webworker-threads which appears to be actively maintained.
Worker Threads reached stable status in 12 LTS. Usage example
const {
Worker, isMainThread, parentPort, workerData
} = require('worker_threads');
if (isMainThread) {
module.exports = function parseJSAsync(script) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, {
workerData: script
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`Worker stopped with exit code ${code}`));
});
});
};
} else {
const { parse } = require('some-js-parsing-library');
const script = workerData;
parentPort.postMessage(parse(script));
}
You can use the child processes, they solve similar problems.
You can look at the specifics of the HTML5 WebWorker source.
With a little care, you can 'redress' the WebWorker to fit as a Node.js worker, by adding a prelude that may look something like this:
const { parentPort } = require('worker_threads')
global.postMessage = function(msg){
parentPort.postMessage(msg)
}
var handler
global.addEventListener = function(kind, callback){
handler = callback
}
parentPort.on('message', msg => {
handler(msg)
})
The specific HTML5 worker added a message event handler using addEventListener, so I registered such a function in global and saved the handler. I also had to supply a postMessage implementation. Finally I registered a Node.js message handler that invokes the HTML5 handler.
Everything works perfectly. No need for any special dependency, just looking at the HTML5 worker code and identify the points where it deals with messages.