// Periodic scheduling for the background context. // // The Chrome MV3 service worker is terminated after roughly 30 seconds idle, // which takes every setInterval/setTimeout with it. The extension alarms API // is the mechanism that survives: the browser holds the schedule and wakes // the worker to deliver onAlarm. Firefox MV2 runs a persistent background // page where timers would survive, but alarms behave identically there, so // both targets share this path and both manifests declare the "alarms" // permission. // // Periods are whole minutes at or above the browser-enforced one-minute // minimum, so nothing here is silently clamped to a slower cadence. // // Trap for anyone changing a period: each job also carries a freshness guard // that can veto its own scheduled tick. A guard timed to the alarm period // halves the real cadence, because the guard is measured from when the last // run finished and the alarm fires one run-duration earlier than that. Every // guard must therefore either be strictly shorter than the period it gates or // be bypassed on the scheduled tick — see backgroundRefresh() in // src/background/index.js. const { alarmsApi } = require("./browserApi"); const BALANCE_REFRESH_ALARM = "autistmask-balance-refresh"; // Alarms this extension used to create and no longer has a handler for. A // browser keeps an alarm until something clears it, so a job that is deleted // from the code goes on waking the service worker on its old schedule forever, // on every install that ever ran the version which created it. Removing the job // means removing the alarm, so retired names are listed here and cleared on // every start until the installs that carry them are long gone. const OBSOLETE_ALARMS = [ // The 24-hour phishing blocklist refresh, retired when the runtime fetch // was removed and the list became purely build-time vendored. "autistmask-phishing-refresh", ]; const MIN_ALARM_PERIOD_MINUTES = 1; const BALANCE_REFRESH_PERIOD_MINUTES = 1; // alarmsApi() resolves on use rather than at module load: the worker is torn // down and re-evaluated repeatedly, and tests install a stub after requiring // this module. It returns null where the API is absent, which is why every // entry point below degrades instead of throwing. /** * Create an alarm unless one with the requested period already exists. * * The existence check is load-bearing: creating an alarm resets its schedule, * and this runs on every worker wake. Creating unconditionally would push the * next fire time out on every incoming message, so a busy extension would * never see the alarm fire at all. * * The period comparison is equally load-bearing in the other direction: an * alarm created by an older version keeps its old period forever unless a * changed constant re-creates it, so a period edit would never reach an * existing install. Re-creating on a period change happens once and then * settles into the existence check above. * * @param {string} name * @param {number} periodInMinutes * @returns {Promise} true if the alarm was created by this call. */ async function ensureAlarm(name, periodInMinutes) { const api = alarmsApi(); if (!api) return false; const period = Math.max(periodInMinutes, MIN_ALARM_PERIOD_MINUTES); const existing = await api.get(name); if (existing && existing.periodInMinutes === period) return false; api.create(name, { periodInMinutes: period, delayInMinutes: period, }); return true; } /** * Clear every alarm this extension no longer handles. * * @returns {Promise} the retired alarms this call actually cleared. */ async function clearObsoleteAlarms() { const api = alarmsApi(); if (!api || !api.clear) return []; const cleared = []; for (const name of OBSOLETE_ALARMS) { if (await api.clear(name)) cleared.push(name); } return cleared; } /** * Ensure the recurring background jobs are scheduled, and that retired ones are * not. Safe to call on every worker start, on onInstalled and on onStartup. * * @returns {Promise<{balance: boolean, cleared: string[]}>} which alarms this * call had to create, and which retired ones it removed. */ async function ensureRecurringAlarms() { const balance = await ensureAlarm( BALANCE_REFRESH_ALARM, BALANCE_REFRESH_PERIOD_MINUTES, ); const cleared = await clearObsoleteAlarms(); return { balance, cleared }; } /** * Register per-alarm handlers. One listener dispatches by alarm name so the * worker only ever installs a single onAlarm listener. * * @param {Object} handlers * @returns {boolean} true if the listener was installed. */ function registerAlarmHandlers(handlers) { const api = alarmsApi(); if (!api || !api.onAlarm) return false; api.onAlarm.addListener((alarm) => { const handler = handlers[alarm && alarm.name]; if (handler) handler(); }); return true; } module.exports = { BALANCE_REFRESH_ALARM, OBSOLETE_ALARMS, MIN_ALARM_PERIOD_MINUTES, BALANCE_REFRESH_PERIOD_MINUTES, clearObsoleteAlarms, ensureAlarm, ensureRecurringAlarms, registerAlarmHandlers, };