diff --git a/unclose/bg.js b/unclose/bg.js index 58405a9..6362674 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -1,43 +1,56 @@ +// Serialize all storage writes to prevent counter races on rapid tab events. +var _writeQueue = Promise.resolve(); +function enqueueWrite(asyncFn) { + _writeQueue = _writeQueue.then(asyncFn).catch((err) => { + console.error('[unclose] storage write failed:', err); + }); + return _writeQueue; +} + chrome.tabs.onUpdated.addListener(async function(tabId, changeInfo, tab) { - await ensureInitialized(); - var tabKey = "TabList-" + tabId; - var currentUrl = tab.url || await storageGet(tabKey); - var updates = {}; - if (tab.url) - updates[tabKey] = tab.url; - updates["TabIndex-" + tabId] = tab.index; - if (tab.favIconUrl) - updates["TabFavicon-" + tabId] = tab.favIconUrl; - if(tab.title != null && currentUrl) - updates["TabTitle-" + tabId] = tab.title; - else if (currentUrl) - updates["TabTitle-" + tabId] = currentUrl; - await storageSet(updates); + await enqueueWrite(async () => { + await ensureInitialized(); + var tabKey = "TabList-" + tabId; + var currentUrl = tab.url || await storageGet(tabKey); + var updates = {}; + if (tab.url) + updates[tabKey] = tab.url; + updates["TabIndex-" + tabId] = tab.index; + if (tab.favIconUrl) + updates["TabFavicon-" + tabId] = tab.favIconUrl; + if (tab.title != null) + updates["TabTitle-" + tabId] = tab.title; + else if (currentUrl) + updates["TabTitle-" + tabId] = currentUrl; + await storageSet(updates); + }); }); -chrome.tabs.onRemoved.addListener(async function(tabId, info) { - await ensureInitialized(); - // Should we record this tab? - var tabKey = "TabList-" + tabId; - var state = await storageGet({ - closeCount: 0, - actualCount: 0, - [tabKey]: null +chrome.tabs.onRemoved.addListener(async function(tabId, info) { + await enqueueWrite(async () => { + await ensureInitialized(); + // Should we record this tab? + var tabKey = "TabList-" + tabId; + var state = await storageGet({ + closeCount: 0, + actualCount: 0, + [tabKey]: null + }); + var url = state[tabKey]; + var re = /^(http:|https:|ftp:|file:)/; + if (url && re.test(url)) { + var closeCount = parseInt(state.closeCount, 10) || 0; + var actualCount = (parseInt(state.actualCount, 10) || 0) + 1; + var updates = { + closeCount: closeCount + 1, + actualCount: actualCount + }; + updates["ClosedTab-" + closeCount] = tabId; + updates["ClosedTabTime-" + closeCount] = new Date().getTime(); + await storageSet(updates); + await setBadgeText(); + } + else + await clear(tabId); }); - var url = state[tabKey]; - var re = /^(http:|https:|ftp:|file:)/; - if (url && re.test(url)) { - var closeCount = parseInt(state.closeCount, 10) || 0; - var actualCount = (parseInt(state.actualCount, 10) || 0) + 1; - var updates = { - closeCount: closeCount + 1, - actualCount: actualCount - }; - updates["ClosedTab-" + closeCount] = tabId; - updates["ClosedTabTime-" + closeCount] = new Date().getTime(); - await storageSet(updates); - await setBadgeText(); - } - else - await clear(tabId); }); diff --git a/unclose/init.js b/unclose/init.js index 7251ea4..6ddf2d0 100644 --- a/unclose/init.js +++ b/unclose/init.js @@ -44,10 +44,13 @@ async function init() await initialize(); } -chrome.runtime.onInstalled.addListener(async function() { - await init(); +chrome.runtime.onInstalled.addListener(async function(details) { + if (details.reason === 'install') await init(); + else await ensureInitialized(); }); chrome.runtime.onStartup.addListener(async function() { - await init(); + // chrome.storage.session is cleared automatically on browser restart; + // just reset the counters and badge. + await initialize(); }); diff --git a/unclose/popup.js b/unclose/popup.js index 57f830d..7531b27 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -131,7 +131,6 @@ function prev() { // Show |url| in a new tab. async function showUrl(tabId) { var state = await storageGet({ - actualCount: 0, ["TabList-" + tabId]: null, ["TabIndex-" + tabId]: null }); @@ -146,8 +145,11 @@ async function showUrl(tabId) { await chrome.tabs.create(createProperties); await clear(tabId); + // Re-read actualCount right before decrementing to minimise the race window + // with concurrent onRemoved increments. + var currentCount = parseInt(await storageGet("actualCount"), 10) || 0; await storageSet({ - actualCount: Math.max((parseInt(state.actualCount, 10) || 0) - 1, 0) + actualCount: Math.max(currentCount - 1, 0) }); await setBadgeText(); await loadContent();