Service worker sync fires only the first time - service-worker

On button click I successfully register for sync and it fires the functionality I've described in the service-worker.
If I'm offline - it waits for the browser to get connection and then it fires.
BUT - when I click the button the first time and all is fine - from then on clicking the button again successfully registers for sync, but the sync event in the service-worker never triggers:
self.addEventListener('sync', function(event) {
console.log('EVENT in SYNC', event);
}
I see the console logging only the first time I click the button.
Am I missing something?

I figured it out :) ... and the problem was pretty lame: the sync handler in the service worker was returning a promise, but this promise never got resolved. Once I added the resolve() part in the handler returned promise - it all worked fine.
PS: In the Jake Archibald's demo the sync handler was doing self.registration.showNotification, which returns a promise, and this promise resolves once the notification is shown. On other examples they make simple fetch, which also returns promise, and that promise resolves on success. But on my example - I was pulling data from indexedDB and then making fetch.
So - just wrapped everything in
var p = new Promise(function(resolve, reject) {
// get data from indexedDB ..
fetch(...).then(function(response) {
return response;
})
.then(function() {
// fetch was successful ..
resolve();
});
};
return p;
This way it worked correctly.

These API's are very experimental and they are most likely to change, so don't take my word for it. I don't have any piece of documentation to support my experience.
In my case, 'sync' event is triggered only once, by design. So I made it work as I wanted by registering to the SyncManager after enqueuing every must-send request:
self.addEventListener('fetch', evt=> {
if(isSuperImportant(evt.request)) {
evt.respondWith(Promise.resolve(new Response({status: 201})));
evt.waitUntil(
myEnqueue(evt.request).then(()=> {
return self.registration.sync.register('sync-tag');
})
);
}
else {
// process the non-important requests
}
});
self.addEventListener('sync', evt=> {
evt.waitUntil(tryToFlushMyQueue());
});

Related

Periodic sync not fired in service worker

I'm trying to use background periodic sync for my website. I'm using localhost and registering the periodicsync event at 1*1000 ms, but that doesn't fire at all.
I had a look at this demo, but even if I install the website as an app, it won't fire at all.
Using chrome 87.0.4280.66.
It works however if I manually trigger the periodic sync event from Application developer window.
The periodicsync event will only register correctly when the app is installed as a PWA in a 'most' webkit based browsers only
https://developer.mozilla.org/en-US/docs/Web/API/Web_Periodic_Background_Synchronization_API
The conditions as to whe this will actually fire is unclear and is dependent on some wooly factors such as the users engagement with the website.
That is why the perameter that can be set when registering for the periodic sync is minInterval
This block will help you to register for it successfully, I am, unfortunatly unclear on what the 'real world' scenarios in which the peridic sync will fire:
const status = await navigator.permissions.query({
// #ts-ignore
name: 'periodic-background-sync',
});
if (status.state === 'granted') {
navigator.serviceWorker.ready.then(async (sw: any) => {
await sw.periodicSync.register('periodicsync', {
minInterval: 1000,
});
})
.catch(error => {
console.error('[BackgroundSync] Error: ' + JSON.stringify(error, null, 2));
});
}
else {
console.error('[BackgroundSync] Does not have permission');
}
}

Redirect API call fetches from Service Worker

This is a really annoying issue. I am using a third party login in my application. When a user logins in through the third party, it redirects an api call to the server.
ex: /api/signin/github?code=test&state=test
For some strange reason this API call is getting fetched from the service worker instead on the server which handles the login logic.
ex:
Without seeing your service worker's fetch event handler, it's hard to say exactly what code is responsible for that.
In general, though, if there are URLs for which you want to tell the service worker never to respond to, you can just avoid calling event.respondWith(...) when they trigger a fetch. There are lots of ways to avoid doing that, but an early return is straightforward:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname === '/api/signin/github') {
// By returning without calling event.respondWith(),
// the request will be handled by the normal browser
// network stack.
return;
}
// Your fetch event response generation logic goes here.
event.respondWith(...);
});

How to offer a page reload when the service worker is updated after a page refresh?

I'm currently using the latest Workbox version 4.3.1 with workbox-window, I'm offering a page reload for users by listening to the waiting event, I'm using the code provided on the Advanced Recipes page of the Workbox documentation.
The code on the page:
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
// Add an event listener to detect when the registered
// service worker has installed but is waiting to activate.
wb.addEventListener('waiting', (event) => {
// `event.wasWaitingBeforeRegister` will be false if this is
// the first time the updated service worker is waiting.
// When `event.wasWaitingBeforeRegister` is true, a previously
// updated same service worker is still waiting.
// You may want to customize the UI prompt accordingly.
// Assumes your app has some sort of prompt UI element
// that a user can either accept or reject.
const prompt = createUIPrompt({
onAccept: async () => {
// Assuming the user accepted the update, set up a listener
// that will reload the page as soon as the previously waiting
// service worker has taken control.
wb.addEventListener('controlling', (event) => {
window.location.reload();
});
// Send a message telling the service worker to skip waiting.
// This will trigger the `controlling` event handler above.
// Note: for this to work, you have to add a message
// listener in your service worker. See below.
wb.messageSW({type: 'SKIP_WAITING'});
},
onReject: () => {
prompt.dismiss();
}
})
});
wb.register();
}
The code in the service worker file:
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
I have tested it and it's working just fine, I can show a snackbar to the user and let them know that there's an update and when they accept, a SKIP_WAITING message is sent to the service worker to call skipWaiting().
Let's assume that the user did not accept the reload prompt and refreshed the page or navigated to another page, the new service worker will be kept waiting and won't activate and that's the normal behavior, but my question is how can I show this reload prompt to the user if they refreshed or navigated to another page? It seems that the waiting event is only fired once.

Why browser loads service worker when going offline and cause "An unknown error occurred when fetching the script."?

I register the service worker with this code:
// If the browser supports serviceWorker, and we haven't registered any - we'll register our: sw.js ..
if ('serviceWorker' in navigator && !navigator.serviceWorker.controller) {
navigator.serviceWorker.register('/sw.js').then(function(registrationObj) {
console.log('Registration object: ', registrationObj);
}).catch(function(error) {
// serviceWorker registration failed ..
console.log('Registration failed with ' + error);
});
} else {
console.log('Service worker already registered. Skip registration.')
};
I see my assets appear in the app cache. Then I go to the Application tab in Chrome, choose Service Workers, click offline and refresh the page.
The page opens fine, but I get this in browser console:
http://www.screencast.com/t/1uodUTHM5ig
and this in the Service Worker debugger:
http://www.screencast.com/t/zmqHMi9RJ
Probably because you do not have service worker in cache (And quite naturally. It is not supposed to be cached in the first place.) so it falls back to standard http fetch process, and it fails as app is offline.
That error does not hinder anything and does not effect how your app works. It is just telling you that it has failed to fetch the script. And it was not suppose to success when the app is offline anyway.
Your service worker script probably structured like this;
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
// request for service worker .js file falls here as it is not in the cache
// this fails of course since the app is offline
return fetch(event.request).then(function (response) {
return response;
});
})
);

A better way to detect a change in a parse class?

Currently I set up a timer that every 2 seconds makes a query to a parse class to see if any data has changed. If the data has changed it calls the refreshData method so my view can be updated with the new parse data.
So when ever data is updated in the parse class it will almost instantly be updated in the app.
The problem is this causes a lot of unnecessary web traffic, which I need to avoid.
What can I do to replace the timer with something that detects when data is changed in the parse class then tells the app to call the refreshData method?
afterSave Triggers
//this will trigger everytime the className objects has changed
Parse.Cloud.afterSave("className", function(request) {
//Do some stuff here //like calling http request or sending push 'data has changed' to installed mobile device
console.log("Object has been added/updated"+request.object.id);
});
https://parse.com/docs/js/guide#cloud-code-aftersave-triggers
You need to deploy first a cloud code then it will handle your problem :-)
In some cases, you may want to perform some action, such as a push, after an object has been saved. You can do this by registering a handler with the afterSave method. For example, suppose you want to keep track of the number of comments on a blog post. You can do that by writing a function like this:
Parse.Cloud.afterSave("Comment", function(request) {
query = new Parse.Query("Post");
query.get(request.object.get("post").id, {
success: function(post) {
post.increment("comments");
post.save();
},
error: function(error) {
console.error("Got an error " + error.code + " : " + error.message);
}
});
});
The client will receive a successful response to the save request after the handler terminates, regardless of how it terminates. For instance, the client will receive a successful response even if the handler throws an exception. Any errors that occurred while running the handler can be found in the Cloud Code log.
If you want to use afterSave for a predefined class in the Parse JavaScript SDK (e.g. Parse.User), you should not pass a String for the first argument. Instead, you should pass the class itself.
I'm not sure if my solution will fit with your needs, but using beforeSave trigger within CloudCode, combined to DirtyKeys will save you time and queries : http://blog.parse.com/learn/engineering/parse-objects-dirtykeys/
With DirtyKeys you can detect once some change was done on your class, and then you can build new trigger and do whatever you need once done.

Resources