Files
AutistMask/src/shared/alarms.js
clawbot ff3387d8cf
All checks were successful
check / check (push) Successful in 27s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 21s
feat: vendor and censor the phishing blocklist at build time (closes #219)
2026-08-17 10:05:56 +02:00

135 lines
5.1 KiB
JavaScript

// 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<boolean>} 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<string[]>} 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<string, function>} 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,
};