Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Service Worker - Wait for clients.openWindow to complete before postMessage

I am using service worker to handle background notifications. When I receive a message, I'm creating a new Notification using self.registration.showNotification(title, { icon, body }). I'm watching for the click event on the notification using self.addEventListener('notificationclick', ()=>{}). On click I'm checking to see if any WindowClient is open, if it is, I'm getting one of those window clients and calling postMessage on it to send the data from the notification to the app to allow the app to process the notification. Incase there is no open window I'm calling openWindow and once that completes I'm sending the data to that window using postMessage.

event.waitUntil(
    clients.matchAll({ type: 'window' }).then((windows) => {
        if (windows.length > 0) {
            const window = windows[0];
            window.postMessage(_data);
            window.focus();
            return;
        }
        return clients.openWindow(this.origin).then((window) => {
            window.postMessage(_data);
            return;
        });
    })
);

The issue I am facing is that the postMessage call inside the openWindow is never delivered. I'm guessing this is because the postMessage call on the WindowClient happens before the page has finished loading, so the eventListener is not registered to listen for that message yet? Is that right?

How do I open a new window from the service worker and postMessage to that new window.

like image 591
Procedurally Generated Avatar asked Nov 26 '22 19:11

Procedurally Generated


1 Answers

I stumble this issue as well, using timeout is anti pattern and also might cause delay larger then the 10 seconds limit of chrome that could fail.

what I did was checking if I need to open a new client window. If I didn't find any match in the clients array - which this is the bottle neck, you need to wait until the page is loaded, and this can take time and postMessage will just not work.

For that case I created in the service worker a simple global object that is being populated in that specific case for example:

const messages = {};
....
// we need to open new window 
messages[randomId] = pushData.message; // save the message from the push notification
await clients.openWindow(urlToOpen + '#push=' + randomId);
....

In the page that is loaded, in my case React app, I wait that my component is mounted, then I run a function that check if the URL includes a '#push=XXX' hash, extracting the random ID, then messaging back to the service worker to send us the message.

...
if (self.location.hash.contains('#push=')) {
  if ('serviceWorker' in navigator && 'Notification' in window && Notification.permission === 'granted') {
     const randomId = self.locaiton.hash.split('=')[1];
     const swInstance = await navigator.serviceWorker.ready;
     if (swInstance) {
        swInstance.active.postMessage({type: 'getPushMessage', id: randomId});
     }
     // TODO: change URL to be without the `#push=` hash ..
}

Then finally in the service worker we add a message event listener:

self.addEventListener('message', function handler(event) {
    if (event.data.type === 'getPushMessage') {
        if (event.data.id && messages[event.data.id]) {
            // FINALLY post message will work since page is loaded
            event.source.postMessage({
                type: 'clipboard',
                msg: messages[event.data.id],
            });
            delete messages[event.data.id];
        }
    }
});

messages our "global" is not persistent which is good, since we just need this when the service worker is "awaken" when a push notification arrives.

The presented code is pseudo code, to point is to explain the idea, which worked for me.

like image 87
Arye Shalev Avatar answered Dec 14 '22 23:12

Arye Shalev