简体   繁体   中英

In a Chrome extension, how to ensure previous promise resolves before the next one using chrome-promise?

I've been using the chrome-promise library to wrap the Chrome extension API with a facade that returns promises instead of using callbacks. This has generally worked quite well, but I seem to be running into an issue with chrome.storage.local APIs.

My extension's event page listens for the chrome.tabs.onActivated and chrome.tabs.onRemoved events. When it gets the onActivated event, it adds the tab info to an array and calls chrome.storage.local.set(data) to store the updated array in local storage.

When it gets the onRemoved event, it calls chromepromise.storage.local.get(null).then(...) to get the list of tabs via a promise, removes the tab info from the array, and then calls chrome.storage.local.set() again to save the updated array.

The issue is that the onActivated event seems to trigger before the promise flow from the onRemoved event resolves. So the onActivated handler retrieves the old stored array, with the closed tab still in it, and then pushes the newly activated tab. So the stored tab data now includes a tab that's already been closed.

I'm assuming this is an issue with using promises instead of callbacks, but I'm wondering if anyone else has run into this problem with this library and worked around it.

Update

As wOxxOm points out , this is a generic problem with "arbitrating unpredictable asynchronous access to a single resource such as chrome.storage " and not unique to the chrome-promise library.

After researching a bit, I came up with a couple solutions, added as answers below. One uses a mutex to ensure (I think) that one promise chain's getting and setting data in chrome.storage completes before the next one starts. The other queues the whole promise chain that's created from an event and doesn't start the next one until the current one has fully completed. I'm not sure which is better, though I suppose locking for a shorter period of time is better.

Any suggestions or better answers are welcome.

Mutex

Update: I ended up using the approach below to create a module that uses a mutex to ensure gets and sets of the Chrome extension storage maintain their order. It seems to be working well so far.


This solution uses the mutex implementation from this article . addTab() and removeTab() call storageMutex.synchronize() with a function that does all the storage getting and setting. This should prevent later events from affecting the storage of earlier events.

The code below is a very simplified version of the extension, but it does run. The playNextEvent() calls at the bottom simulate opening 4 tabs, switching back to tab 2 and closing it, which then causes tab 3 to activate. setTimeout() s are used so that everything doesn't run as one long call stack.

 function Mutex() { this._busy = false; this._queue = []; } Object.assign(Mutex.prototype, { synchronize: function(task) { var self = this; return new Promise(function(resolve, reject) { self._queue.push([task, resolve, reject]); if (!self._busy) { self._dequeue(); } }); }, _dequeue: function() { var next = this._queue.shift(); if (next) { this._busy = true; this._execute(next); } else { this._busy = false; } }, _execute: function(record) { var task = record[0], resolve = record[1], reject = record[2], self = this; task().then(resolve, reject).then(function() { self._dequeue(); }); } }); const storageMutex = new Mutex(); function onActivated(tabID) { console.log("EVENT onActivated", tabID); return Promise.resolve(tabID).then(tab => addTab(tab)); } function onRemoved(tabID) { console.log("EVENT onRemoved", tabID); return removeTab(tabID); } var localData = { tabs: [] }; function delay(time) { return new Promise(resolve => setTimeout(resolve, time)); } function getData() { return delay(0).then(() => JSON.parse(JSON.stringify(localData))); } function saveData(data, source) { return delay(0) .then(() => { localData = data; console.log("save from:", source, "localData:", localData); return Promise.resolve(localData); }); } function addTab(tabID) { return storageMutex.synchronize(() => getData().then((data) => { console.log("addTab", tabID, "data:", data); data.tabs = data.tabs.filter(tab => tab != tabID); data.tabs.push(tabID); return saveData(data, "addTab"); })); } function removeTab(tabID) { return storageMutex.synchronize(() => getData().then((data) => { console.log("removeTab", tabID, "data:", data); data.tabs = data.tabs.filter(tab => tab != tabID); return saveData(data, "removeTab"); })); } const events = [ () => onActivated(1), () => onActivated(2), () => onActivated(3), () => onActivated(4), () => onActivated(2), () => { onRemoved(2); onActivated(3) } ]; function playNextEvent() { var event = events.shift(); if (event) { delay(0).then(() => { event(); delay(0).then(playNextEvent) }); } } playNextEvent(); 

Queue

This solution uses a very simple queuing mechanism. The event handlers call queue() with a function that kicks off the promise chain to handle that event. If there isn't already a promise in the queue, then the function is called immediately. Otherwise, it's pushed on the queue and will be triggered when the current promise chain finishes. This means only one event can be processed at a time, which might not be as efficient.

 var taskQueue = []; function queue( fn) { taskQueue.push(fn); processQueue(); } function processQueue() { const nextTask = taskQueue[0]; if (nextTask && !(nextTask instanceof Promise)) { taskQueue[0] = nextTask() .then((result) => { console.log("RESULT", result); taskQueue.shift(); processQueue(); }); } } function onActivated(tabID) { console.log("EVENT onActivated", tabID); queue(() => Promise.resolve(tabID).then(tab => addTab(tab))); } function onRemoved(tabID) { console.log("EVENT onRemoved", tabID); queue(() => removeTab(tabID)); } var localData = { tabs: [] }; function delay(time) { return new Promise(resolve => setTimeout(resolve, time)); } function getData() { return delay(0).then(() => JSON.parse(JSON.stringify(localData))); } function saveData(data, source) { return delay(0) .then(() => { localData = data; console.log("save from:", source, "localData:", localData); return Promise.resolve(localData); }); } function addTab(tabID) { return getData().then((data) => { console.log("addTab", tabID, "data:", data); data.tabs = data.tabs.filter(tab => tab != tabID); data.tabs.push(tabID); return saveData(data, "addTab"); }); } function removeTab(tabID) { return getData().then((data) => { console.log("removeTab", tabID, "data:", data); data.tabs = data.tabs.filter(tab => tab != tabID); return saveData(data, "removeTab"); }); } const events = [ () => onActivated(1), () => onActivated(2), () => onActivated(3), () => onActivated(4), () => onActivated(2), () => { onRemoved(2); onActivated(3) } ]; function playNextEvent() { var event = events.shift(); if (event) { delay(0).then(() => { event(); delay(0).then(playNextEvent) }); } } playNextEvent(); 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM