javascriptindexeddbservice-workerweb-pushpush-api

Push WebAPI + IndexedDB + ServiceWorker


I've implemented the Push WebAPI in my web application using Service Worker as many articles explain on the web. Now I need to store some data inside IndexedDB to make them available while the web app is closed (chrome tab closed, service worker in background execution). In particular I would like to store a simple url from where retrieve the notification data (from server).

Here is my code:

    self.addEventListener("push", (event) => {
    console.log("[serviceWorker] Push message received", event);

    notify({ event: "push" }); // This notifies the push service for handling the notification

    var open = indexedDB.open("pushServiceWorkerDb", 1);
    open.onsuccess = () => {
        var db = open.result;
        var tx = db.transaction("urls");
        var store = tx.objectStore("urls");
        var request = store.get("fetchNotificationDataUrl");

        request.onsuccess = (ev) => {
            var fetchNotificationDataUrl = request.result;
            console.log("[serviceWorker] Fetching notification data from ->", fetchNotificationDataUrl);

            if (!(!fetchNotificationDataUrl || fetchNotificationDataUrl.length === 0 || !fetchNotificationDataUrl.trim().length === 0)) {
                event.waitUntil(
                    fetch(fetchNotificationDataUrl, {
                        credentials: "include"
                    }).then((response) => {
                        if (response.status !== 200) {  
                            console.log("[serviceWorker] Looks like there was a problem. Status Code: " + response.status);
                            throw new Error();
                        }

                        return response.json().then((data) => {
                            if (!data) {
                                console.error("[serviceWorker] The API returned no data. Showing default notification", data);
                                //throw new Error();
                                showDefaultNotification({ url: "/" });
                            }

                            var title = data.Title;
                            var message = data.Message;
                            var icon = data.Icon;
                            var tag = data.Tag;
                            var url = data.Url;

                            return self.registration.showNotification(title, {
                                body: message,
                                icon: icon,
                                tag: tag,
                                data: {
                                    url: url
                                },
                                requireInteraction: true
                            });
                        });
                    }).catch((err) => {
                        console.error("[serviceWorker] Unable to retrieve data", err);

                        var title = "An error occurred";
                        var message = "We were unable to get the information for this push message";
                        var icon = "/favicon.ico";
                        var tag = "notification-error";
                        return self.registration.showNotification(title, {
                            body: message,
                            icon: icon,
                            tag: tag,
                            data: {
                                url: "/"
                            },
                            requireInteraction: true
                        });
                    })
                );
            } else {
                showDefaultNotification({ url: "/" });
            }
        }
    };
});

Unfortunately when I receive a new push event it doesn't work, showing this exception:

Uncaught DOMException: Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished. at IDBRequest.request.onsuccess (https://192.168.0.102/pushServiceWorker.js:99:23)

How can I resolve this?

Thanks in advance


Solution

  • The initial call to event.waitUntil() needs to be done synchronously when the event handler is first invoked. You can then pass in a promise chain to event.waitUntil(), and inside that promise chain, carry out any number of asynchronous actions.

    Your current code invokes an asynchronous IndexedDB callback before it calls event.waitUntil(), which is why you're seeing that error.

    The easiest way to include IndexedDB operations inside a promise chain is to use a wrapper library, like idb-keyval, which takes the callback-based IndexedDB API and converts it into a promise-based API.

    Your code could then look like:

    self.addEventListener('push', event => {
      // Call event.waitUntil() immediately:
      event.waitUntil(
        // You can chain together promises:
        idbKeyval.get('fetchNotificationDataUrl')
          .then(url => fetch(url))
          .then(response => response.json())
          .then(json => self.registration.showNotification(...)
      );
    });