0

self.setTimeout reliably fires self.registration.showNotification after the expected delay, only under the condition the browser is NOT minimized / hidden.

It seems to work up to 20 seconds later. After that, it silently fails.

I have not yet determined if it is the self.setTimeout which fails to run the callback, or if it is the self.registration.showNotification fails to show the notification.

Code:

importScripts('./ngsw-worker.js');

let pendingNotifications = new Map();

function wait(ms, pendingNotification) {
  return new Promise(resolve => {
    pendingNotification.TimerId = setTimeout(resolve, ms);
  });
}

async function processNotification(data, pendingNotification) {
  let delay = await wait(data.Data.DeferredSeconds * 1000, pendingNotification);
  //let notCancelled = pendingNotifications.has(data.Data.CancellationToken);
  pendingNotifications.delete(data.Data.CancellationToken);
  //if (notCancelled) {
  self.registration.showNotification(data.Data.Title, { icon: data.Data.Icon, vibrate: data.Data.VibrationPattern, body: data.Data.Body });
  //}
  return null;
}

self.addEventListener('message', function (messageEvent) {
  let data = messageEvent.data;
  if (data == null) {
    return;
  }

  if (data.Type == "Notification") {

    let pendingNotification = {
      TimerId: -1,
      CancellationToken: data.Data.CancellationToken
    };
    pendingNotifications.set(data.Data.CancellationToken, pendingNotification);

    messageEvent.waitUntil(processNotification(data, pendingNotification));

  }

  if (data.Type == "NotificationCancel") {
    let pendingNotification = pendingNotifications.get(data.Data.CancellationToken);
    if (pendingNotification == null) {
      return;
    }
    self.clearTimeout(pendingNotification.TimerId);
    pendingNotifications.delete(pendingNotification.CancellationToken);
  }


})
applejacks01
  • 249
  • 1
  • 18

1 Answers1

0

A service worker is aggressively killed by the browser when it "appears" to be idle.

If you have asynchronous work that you want to perform while keeping the service worker alive, you need to perform that work inside of a Promise which you then pass to event.waitUntil(). This does not keep the service worker alive forever, but it does tell the browser to extend the service worker's lifetime until either the Promise resolves or a browser-specific time limit (usually a few minutes) has elapsed.

Any service worker event that inherits from ExtendableEvent, including MessageEvent, exposes a waitUntil() method.

You'll need to rewrite your code a bit to translate the setTimeout() calls into something that's Promise-friendly, though.

Jeff Posnick
  • 53,580
  • 14
  • 141
  • 167
  • Hi Jeff, thanks. I updated code in the OP. Does that look correct to you? I just tested it out with a timer set for 5 minutes, and it didn't work unfortunately. You mentioned a "browser-specific time limit" ... – applejacks01 Oct 08 '20 at 21:31
  • By the way, on Desktop Chrome, a 5 minute timer worked. On Android Chrome, the 5 minute timer eventually fired after approximately 8 minutes. I re-opened the browser at the 5 minute mark, then minimized it after. – applejacks01 Oct 08 '20 at 21:34
  • I can't run the code myself, but I think that's the general idea. The amount of time each browser will wait for a `Promise` passed to `waitUntil()` to resolve before killing the SW will vary from browser to browser, and operating system to operating system. There are no guarantees, unfortunately. – Jeff Posnick Oct 09 '20 at 14:20