[PM-6426] Create TaskSchedulerService and update usage of long lived timeouts
This commit is contained in:
parent
2e51d96416
commit
66ebdc04c8
|
@ -591,4 +591,24 @@ export class BrowserApi {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static clearAlarm(alarmName: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
static clearAllAlarms(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => chrome.alarms.clearAll(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
static createAlarm(name: string, createInfo: chrome.alarms.AlarmCreateInfo): Promise<void> {
|
||||||
|
return new Promise((resolve) => chrome.alarms.create(name, createInfo, resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAlarm(alarmName: string): Promise<chrome.alarms.Alarm> {
|
||||||
|
return new Promise((resolve) => chrome.alarms.get(alarmName, resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAllAlarms(): Promise<chrome.alarms.Alarm[]> {
|
||||||
|
return new Promise((resolve) => chrome.alarms.getAll(resolve));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { TaskSchedulerService } from "@bitwarden/common/platform/abstractions/task-scheduler.service";
|
||||||
|
import { ScheduledTaskName } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
|
||||||
|
|
||||||
|
export type ActiveAlarm = {
|
||||||
|
name: ScheduledTaskName;
|
||||||
|
startTime: number;
|
||||||
|
createInfo: chrome.alarms.AlarmCreateInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BrowserTaskSchedulerService extends TaskSchedulerService {
|
||||||
|
clearAllScheduledTasks(): Promise<void>;
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { firstValueFrom, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { TaskIdentifier } from "@bitwarden/common/platform/abstractions/task-scheduler.service";
|
||||||
|
import { ScheduledTaskName } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
|
||||||
|
import { TaskSchedulerService } from "@bitwarden/common/platform/services/task-scheduler.service";
|
||||||
|
import {
|
||||||
|
SCHEDULED_TASKS_DISK,
|
||||||
|
GlobalState,
|
||||||
|
KeyDefinition,
|
||||||
|
StateProvider,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveAlarm,
|
||||||
|
BrowserTaskSchedulerService as BrowserTaskSchedulerServiceInterface,
|
||||||
|
} from "./abstractions/browser-task-scheduler.service";
|
||||||
|
|
||||||
|
const ACTIVE_ALARMS = new KeyDefinition(SCHEDULED_TASKS_DISK, "activeAlarms", {
|
||||||
|
deserializer: (value: ActiveAlarm[]) => value ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export class BrowserTaskSchedulerService
|
||||||
|
extends TaskSchedulerService
|
||||||
|
implements BrowserTaskSchedulerServiceInterface
|
||||||
|
{
|
||||||
|
private activeAlarmsState: GlobalState<ActiveAlarm[]>;
|
||||||
|
readonly activeAlarms$: Observable<ActiveAlarm[]>;
|
||||||
|
private recoveredAlarms: Set<string> = new Set();
|
||||||
|
private onAlarmHandlers: Record<string, () => void> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private logService: LogService,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.activeAlarmsState = this.stateProvider.getGlobal(ACTIVE_ALARMS);
|
||||||
|
this.activeAlarms$ = this.activeAlarmsState.state$.pipe(
|
||||||
|
map((activeAlarms) => activeAlarms ?? []),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setupOnAlarmListener();
|
||||||
|
this.verifyAlarmsState().catch((e) => this.logService.error(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTimeout(
|
||||||
|
callback: () => void,
|
||||||
|
delayInMs: number,
|
||||||
|
taskName?: ScheduledTaskName,
|
||||||
|
): Promise<number | NodeJS.Timeout> {
|
||||||
|
const delayInMinutes = delayInMs / 1000 / 60;
|
||||||
|
if (delayInMinutes < 1) {
|
||||||
|
return super.setTimeout(callback, delayInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerAlarmHandler(taskName, callback);
|
||||||
|
if (this.recoveredAlarms.has(taskName)) {
|
||||||
|
await this.triggerRecoveredAlarm(taskName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.createAlarm(taskName, { delayInMinutes });
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInterval(
|
||||||
|
callback: () => void,
|
||||||
|
intervalInMs: number,
|
||||||
|
taskName?: ScheduledTaskName,
|
||||||
|
initialDelayInMs?: number,
|
||||||
|
): Promise<number | NodeJS.Timeout> {
|
||||||
|
const intervalInMinutes = intervalInMs / 1000 / 60;
|
||||||
|
if (intervalInMinutes < 1) {
|
||||||
|
return super.setInterval(callback, intervalInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerAlarmHandler(taskName, callback);
|
||||||
|
if (this.recoveredAlarms.has(taskName)) {
|
||||||
|
await this.triggerRecoveredAlarm(taskName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialDelayInMinutes = initialDelayInMs ? initialDelayInMs / 1000 / 60 : undefined;
|
||||||
|
await this.createAlarm(taskName, {
|
||||||
|
periodInMinutes: intervalInMinutes,
|
||||||
|
delayInMinutes: initialDelayInMinutes ?? intervalInMinutes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearScheduledTask(taskIdentifier: TaskIdentifier): Promise<void> {
|
||||||
|
void super.clearScheduledTask(taskIdentifier);
|
||||||
|
|
||||||
|
const { taskName } = taskIdentifier;
|
||||||
|
if (!taskName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasCleared = await BrowserApi.clearAlarm(taskName);
|
||||||
|
if (wasCleared) {
|
||||||
|
await this.deleteActiveAlarm(taskName);
|
||||||
|
this.recoveredAlarms.delete(taskName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAllScheduledTasks(): Promise<void> {
|
||||||
|
await BrowserApi.clearAllAlarms();
|
||||||
|
await this.updateActiveAlarms([]);
|
||||||
|
this.onAlarmHandlers = {};
|
||||||
|
this.recoveredAlarms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAlarm(
|
||||||
|
name: ScheduledTaskName,
|
||||||
|
createInfo: chrome.alarms.AlarmCreateInfo,
|
||||||
|
): Promise<void> {
|
||||||
|
const existingAlarm = await BrowserApi.getAlarm(name);
|
||||||
|
if (existingAlarm) {
|
||||||
|
this.logService.debug(`Alarm ${name} already exists. Skipping creation.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await BrowserApi.createAlarm(name, createInfo);
|
||||||
|
await this.setActiveAlarm({ name, startTime: Date.now(), createInfo });
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerAlarmHandler(name: ScheduledTaskName, handler: CallableFunction): void {
|
||||||
|
if (this.onAlarmHandlers[name]) {
|
||||||
|
this.logService.warning(`Alarm handler for ${name} already exists. Overwriting.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onAlarmHandlers[name] = () => handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyAlarmsState(): Promise<void> {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const activeAlarms = await firstValueFrom(this.activeAlarms$);
|
||||||
|
|
||||||
|
for (const alarm of activeAlarms) {
|
||||||
|
const { name, startTime, createInfo } = alarm;
|
||||||
|
const existingAlarm = await BrowserApi.getAlarm(name);
|
||||||
|
if (existingAlarm) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(createInfo.when && createInfo.when < currentTime) ||
|
||||||
|
(!createInfo.periodInMinutes &&
|
||||||
|
createInfo.delayInMinutes &&
|
||||||
|
startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime)
|
||||||
|
) {
|
||||||
|
this.recoveredAlarms.add(name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.createAlarm(name, createInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10 seconds after verifying the alarm state, we should treat any newly created alarms as non-recovered alarms.
|
||||||
|
setTimeout(() => this.recoveredAlarms.clear(), 10 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setActiveAlarm(alarm: ActiveAlarm): Promise<void> {
|
||||||
|
const activeAlarms = await firstValueFrom(this.activeAlarms$);
|
||||||
|
activeAlarms.push(alarm);
|
||||||
|
await this.updateActiveAlarms(activeAlarms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteActiveAlarm(name: ScheduledTaskName): Promise<void> {
|
||||||
|
const activeAlarms = await firstValueFrom(this.activeAlarms$);
|
||||||
|
const filteredAlarms = activeAlarms.filter((alarm) => alarm.name !== name);
|
||||||
|
await this.updateActiveAlarms(filteredAlarms);
|
||||||
|
delete this.onAlarmHandlers[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateActiveAlarms(alarms: ActiveAlarm[]): Promise<void> {
|
||||||
|
await this.activeAlarmsState.update(() => alarms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async triggerRecoveredAlarm(name: ScheduledTaskName): Promise<void> {
|
||||||
|
this.recoveredAlarms.delete(name);
|
||||||
|
await this.triggerAlarm(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupOnAlarmListener(): void {
|
||||||
|
BrowserApi.addListener(chrome.alarms.onAlarm, this.handleOnAlarm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOnAlarm = async (alarm: chrome.alarms.Alarm): Promise<void> => {
|
||||||
|
await this.triggerAlarm(alarm.name as ScheduledTaskName);
|
||||||
|
};
|
||||||
|
|
||||||
|
private async triggerAlarm(name: ScheduledTaskName): Promise<void> {
|
||||||
|
const handler = this.onAlarmHandlers[name];
|
||||||
|
if (handler) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
const alarm = await BrowserApi.getAlarm(name);
|
||||||
|
if (alarm?.periodInMinutes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.deleteActiveAlarm(name);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ScheduledTaskName } from "../enums/scheduled-task-name.enum";
|
||||||
|
|
||||||
|
export type TaskIdentifier = {
|
||||||
|
taskName?: ScheduledTaskName;
|
||||||
|
timeoutId?: number | NodeJS.Timeout;
|
||||||
|
intervalId?: number | NodeJS.Timeout;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TaskSchedulerService {
|
||||||
|
setTimeout(
|
||||||
|
callback: () => void,
|
||||||
|
delayInMs: number,
|
||||||
|
taskName?: ScheduledTaskName,
|
||||||
|
): Promise<number | NodeJS.Timeout>;
|
||||||
|
setInterval(
|
||||||
|
callback: () => void,
|
||||||
|
intervalInMs: number,
|
||||||
|
taskName?: ScheduledTaskName,
|
||||||
|
initialDelayInMs?: number,
|
||||||
|
): Promise<number | NodeJS.Timeout>;
|
||||||
|
clearScheduledTask(taskIdentifier: TaskIdentifier): Promise<void>;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const ScheduledTaskNames = {
|
||||||
|
clearClipboardTimeout: "clearClipboardTimeout",
|
||||||
|
systemClearClipboardTimeout: "systemClearClipboardTimeout",
|
||||||
|
scheduleNextSyncTimeout: "scheduleNextSyncTimeout",
|
||||||
|
loginStrategySessionTimeout: "loginStrategySessionTimeout",
|
||||||
|
notificationsReconnectTimeout: "notificationsReconnectTimeout",
|
||||||
|
fido2ClientAbortTimeout: "fido2ClientAbortTimeout",
|
||||||
|
eventUploadsInterval: "eventUploadsInterval",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ScheduledTaskName = (typeof ScheduledTaskNames)[keyof typeof ScheduledTaskNames];
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {
|
||||||
|
TaskIdentifier,
|
||||||
|
TaskSchedulerService as TaskSchedulerServiceInterface,
|
||||||
|
} from "../abstractions/task-scheduler.service";
|
||||||
|
import { ScheduledTaskName } from "../enums/scheduled-task-name.enum";
|
||||||
|
|
||||||
|
export class TaskSchedulerService implements TaskSchedulerServiceInterface {
|
||||||
|
async setTimeout(
|
||||||
|
callback: () => void,
|
||||||
|
delayInMs: number,
|
||||||
|
_taskName?: ScheduledTaskName,
|
||||||
|
): Promise<number | NodeJS.Timeout> {
|
||||||
|
return setTimeout(() => callback(), delayInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInterval(
|
||||||
|
callback: () => void,
|
||||||
|
intervalInMs: number,
|
||||||
|
_taskName?: ScheduledTaskName,
|
||||||
|
_initialDelayInMs?: number,
|
||||||
|
): Promise<number | NodeJS.Timeout> {
|
||||||
|
return setInterval(() => callback(), intervalInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearScheduledTask(taskIdentifier: TaskIdentifier): Promise<void> {
|
||||||
|
if (taskIdentifier.timeoutId) {
|
||||||
|
clearTimeout(taskIdentifier.timeoutId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskIdentifier.intervalId) {
|
||||||
|
clearInterval(taskIdentifier.intervalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,6 +87,7 @@ export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||||
export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory");
|
export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory");
|
||||||
export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
|
export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
|
||||||
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
|
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
|
||||||
|
export const SCHEDULED_TASKS_DISK = new StateDefinition("scheduledTasks", "disk");
|
||||||
|
|
||||||
// Secrets Manager
|
// Secrets Manager
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue