Service Worker:如何同步queue?

Service Worker: how to do a synchronous queue?

我有一个 Service Worker 从 Firebase FCM 接收推送消息。它们会导致通知显示或取消。用户可以有多个设备(这就是取消的目的:当用户已经对通知 A 采取行动时,我会尝试在所有设备上关闭它)。

我遇到的问题是用户的其中一台设备离线或完全关闭。设备上线后,firebase 会传送它之前无法传送的所有消息。因此,例如,您会得到:

SW 快速连续接收这些消息。问题是取消通知比显示通知快得多(~2 毫秒对 16 毫秒)。所以第四条消息在第一条(或第二条)消息实际创建通知之前被处理,结果是通知没有被取消。

// 编辑: 下面是经过大量编辑的问题。添加了示例代码并分解了我的问题。还编辑了标题以更好地反映我实际的基本问题。

我尝试将消息推送到 queue 中并一条一条地处理它们。事实证明这可能会变得有点复杂,因为 SW 中的所有内容都是异步的,更糟糕​​的是,当浏览器认为 SW 完成其工作时,它可以随时被终止。我试图以持久的方式存储 queue,但由于 LocalStorage 在 SW 中不可用,我需要使用异步 IndexedDB API。更多可能导致问题的异步调用(如丢失物品)。

也有可能 event.waitUntil 认为我的工人在实际完成之前就已经完成了,因为我没有正确地 'passing the torch' 从承诺到承诺..

这是我尝试过的(很多)简化代码:

// Use localforage, simplified API for IndexedDB
importScripts("localforage.min.js");

// In memory..
var mQueue = []; // only accessed through get-/setQueue()
var mQueueBusy = false;

// Receive push messages..
self.addEventListener('push', function(event) {
    var data = event.data.json().data;
    event.waitUntil(addToQueue(data));
});

// Add to queue
function addToQueue(data) {
    return new Promise(function(resolve, reject) {
        // Get queue..
        getQueue()
        .then(function(queue) {
            // Push + store..
            queue.push(data);
            setQueue(queue)
            .then(function(queue){
                handleQueue()
                .then(function(){
                    resolve();
                });
            });
        });
    });
}

// Handle queue
function handleQueue(force) {
    return new Promise(function(resolve, reject) {
        // Check if busy
        if (mQueueBusy && !force) {
            resolve();
        } else {
            // Set busy..
            mQueueBusy = true;
            // Get queue..
            getQueue()
            .then(function(queue) {
                // Check if we're done..
                if (queue && queue.length<=0) {
                    resolve();
                } else {
                    // Shift first item
                    var queuedData = queue.shift();
                    // Store before continuing.. 
                    setQueue(queue)
                    .then(function(queue){
                        // Now do work here..
                        doSomething(queuedData)
                        .then(function(){
                            // Call handleQueue with 'force=true' to go past (mQueueBusy)
                            resolve(handleQueue(true));
                        });
                    });
                }
            });
        }
    });
}

// Get queue
function getQueue() {
    return new Promise(function(resolve, reject) {
        // Get from memory if it's there..
        if (mQueue && mQueue.length>0) {
            resolve(mQueue);
        } 
        // Read from indexed db..
        else {
            localforage.getItem("queue")
            .then(function(val) {
                var queue = (val) ? JSON.parse(val) : [];
                mQueue = queue;
                resolve(mQueue);
            });
        }
    });
}

// Set queue
function setQueue(queue) {
    return new Promise(function(resolve, reject) {
        // Store queue to memory..
        mQueue = queue;
        // Write to indexed db..
        localforage.setItem("queue", mQueue)
        .then(function(){
            resolve(mQueue);
        });
    });
}

// Do something..
function doSomething(queuedData) {
    return new Promise(function(resolve, reject) {
        // just print something and resolve
        console.log(queuedData);
        resolve();
    });
}

我的问题的简短版本 - 考虑到我的特定 use-case - 是:我如何同步处理推送消息而不必使用更多异步 API?

如果我将这些问题分成多个:

只是为了理解 "why not use collapse_key":这是一个聊天应用程序,每个聊天室都有自己的标签。一个用户可以参与 4 个以上的聊天室,由于 firebase 将 collapse_keys 的数量限制为 4 个,我不能使用它。

所以我要大胆地说,将内容序列化到 IDB 可能有点矫枉过正。只要您等到所有待处理的工作都完成之后再解决传递给 event.waitUntil() 的承诺,服务工作者就应该保持活动状态。 (如果需要几分钟才能完成这项工作,那么 service worker 无论如何都有可能被杀死,但对于你所描述的情况,我认为这种风险很低。)

下面是我将如何构建您的代码的粗略草图,利用当前支持服务工作者的所有浏览器中的本机 async/await 支持。

(我还没有实际测试过这些,但从概念上讲我认为它是合理的。)

// In your service-worker.js:

const isPushMessageHandlerRunning = false;
const queue = [];

self.addEventListener('push', event => {
  var data = event.data.json().data;
  event.waitUntil(queueData(data));
});

async function queueData(data) {
  queue.push(data);
  if (!isPushMessageHandlerRunning) {
    await handlePushDataQueue();
  }
}

async function handlePushDataQueue() {
  isPushMessageHandlerRunning = true;

  let data;
  while(data = queue.shift()) {
    // Await on something asynchronous, based on data.
    // e.g. showNotification(), getNotifications() + notification.close(), etc.
    await ...;
  }

  isPushMessageHandlerRunning = false;
}