[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 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
|
||||
|
||||
|
|
Loading…
Reference in New Issue