1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-08-27 23:31:41 +02:00

[PM-6426] Create TaskSchedulerService and update usage of long lived timeouts

This commit is contained in:
Cesar Gonzalez 2024-04-01 09:28:19 -05:00
parent 2e51d96416
commit 66ebdc04c8
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
7 changed files with 307 additions and 0 deletions

View File

@ -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));
}
}

View File

@ -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>;
}

View File

@ -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);
}
}

View File

@ -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>;
}

View File

@ -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];

View File

@ -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);
}
}
}

View File

@ -87,6 +87,7 @@ export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory");
export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
export const SCHEDULED_TASKS_DISK = new StateDefinition("scheduledTasks", "disk");
// Secrets Manager