Why CookieMop waits 15 seconds after you close a tab, and why that is the hard part in Manifest V3
CookieMop is a Chrome extension that deletes a site's cookies when you close its last tab, unless the site is on your whitelist. One sentence of product. The interesting engineering is in three words of that sentence: "l
CookieMop is a Chrome extension that deletes a site's cookies when you close its last tab, unless the site is on your whitelist. One sentence of product. The interesting engineering is in three words of that sentence: "last", "close", and the delay hiding between them.
Why not delete immediately
Closing a tab by accident is common, and cookies are what keep you logged in. So the extension does not clean on the close event. It schedules a cleanup 15 seconds later (the default; users can change it, and zero means immediate) and only then checks whether the site is still gone. Reopen the tab within the window and the pending cleanup is cancelled. One Playwright test exists for exactly that path: revisiting the site during the delay cancels the cleanup.
That sounds like a setTimeout. In Manifest V3 it cannot be only a setTimeout.
The service worker will not wait for you
In Manifest V3 the background script is a service worker, and Chrome can terminate it whenever it is idle. A timer living inside a worker that gets killed at second 9 of a 15-second wait never fires. The old Manifest V2 answer, a persistent background page, is gone.
The tool Chrome offers instead is chrome.alarms, which survives the worker being unloaded. It has one catch that shaped the whole design: alarms are clamped to a minimum of 30 seconds. A 15-second delay cannot be expressed as an alarm.
So the extension does both. While the worker is alive, a plain timer fires the cleanup on time. In parallel, every pending cleanup is written to storage with its fireAt timestamp, and one alarm is set for the earliest of them, pushed out to at least 30 seconds if needed:
const ALARM_MIN_MS = 30_000; // Chrome clamps MV3 alarms to a 30 s minimum
const earliest = Math.min(...pendings.map((p) => p.fireAt));
chrome.alarms.create(ALARM_NAME, { when: Math.max(earliest, now + ALARM_MIN_MS) });
If the worker lives, the timer wins and the alarm finds nothing to do. If the worker dies, the alarm wakes a fresh worker, which reads the pending list from storage and processes whatever is due. There is a test for that too: kill the service worker, and the alarm still fires the pending cleanup. The cost of the fallback is that a cleanup can land at 30 seconds instead of 15 when Chrome happened to unload the worker. I decided that late is fine and never is not.
"Last tab" means last tab of the site, not the page
The second word. If you have mail.google.com in one tab and docs.google.com in another, closing one must not log you out of the other. So the unit of tracking is not the hostname but the registrable domain, the eTLD+1: mail.google.com becomes google.com, and a.b.example.co.uk becomes example.co.uk.
Doing that properly means knowing which suffixes are "public", and the full Public Suffix List is thousands of entries. CookieMop ships no dependencies and no build step, so it carries a compact table of the common two-part suffixes instead: co.uk, co.kr, com.au, co.jp, 257 entries in total. A hostname ending in one of those needs three labels to be a site; anything else needs two. IP addresses and single-label hosts like localhost are left as they are. It is not the whole list, and I would rather say that here than pretend. It covers the cases people actually log in to.
The same table decides the lists. A whitelist rule on a subdomain survives a cleanup of its parent domain, and a more specific whitelist entry beats a greylist entry. Greylisted sites are the third state: they survive tab close and are cleaned when the browser restarts, in a startup pass that runs before you have opened anything.
The race nobody sees
There is a comment in the background script that I keep because it cost an afternoon. Recording a tab's domain on tabs.onUpdated and taking it back on tabs.onRemoved are both async read-then-write round trips to storage. If a tab is opened and closed fast enough, the "remove" read can overtake the still-pending "record" write, and the extension thinks a site was never open. The fix is a small promise queue that serializes every tab-record read and write, and the reason it is worth writing down is that it never shows up in manual testing. It shows up in a Playwright run that opens and closes tabs faster than a person can.
What "clean" means
By default only cookies are removed. The user can widen the scope to localStorage, or to all site data: IndexedDB, CacheStorage and service worker registrations, through chrome.browsingData. Widening it is a real trade-off, because the same storage that tracks you also holds a half-written draft on some sites, so the narrower default stays.
Everything runs locally. The extension makes no network requests, has no accounts and no analytics, and the repository on GitHub is the code that ships to the store. If you want to check the timer logic yourself, src/background.js is about 430 lines and the alarm fallback is the part worth reading first.
Have you ever lost a login to a tab you closed by mistake, or is 15 seconds too long for the way you browse?
The extension is here: https://chromewebstore.google.com/detail/nbehnialaodjcffgjkckbnocggbdmdel
Sources
- chrome.alarms API, minimum alarm period in Manifest V3: https://developer.chrome.com/docs/extensions/reference/api/alarms
- Extension service worker lifecycle (idle termination): https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle
- Public Suffix List, what "registrable domain" means: https://publicsuffix.org/
- CookieMop source, the exact code shipped to the Chrome Web Store: https://github.com/thoopring/cookiemop
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.