[PM-6426] Attempting to re-work implementation to facilitate userId-spcific alarms

This commit is contained in:
Cesar Gonzalez 2024-04-17 16:16:13 -05:00
parent 6cb2577f90
commit 6beab4fbd2
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
5 changed files with 72 additions and 57 deletions

View File

@ -2,7 +2,7 @@ import { TaskSchedulerService } from "@bitwarden/common/platform/abstractions/ta
import { ScheduledTaskName } from "@bitwarden/common/platform/enums/scheduled-task-name.enum"; import { ScheduledTaskName } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
export type ActiveAlarm = { export type ActiveAlarm = {
name: ScheduledTaskName; taskName: ScheduledTaskName;
startTime: number; startTime: number;
createInfo: chrome.alarms.AlarmCreateInfo; createInfo: chrome.alarms.AlarmCreateInfo;
}; };

View File

@ -30,15 +30,15 @@ describe("BrowserTaskSchedulerService", () => {
jest.useFakeTimers(); jest.useFakeTimers();
activeAlarms = [ activeAlarms = [
mock<ActiveAlarm>({ mock<ActiveAlarm>({
name: ScheduledTaskNames.eventUploadsInterval, taskName: ScheduledTaskNames.eventUploadsInterval,
createInfo: eventUploadsIntervalCreateInfo, createInfo: eventUploadsIntervalCreateInfo,
}), }),
mock<ActiveAlarm>({ mock<ActiveAlarm>({
name: ScheduledTaskNames.scheduleNextSyncInterval, taskName: ScheduledTaskNames.scheduleNextSyncInterval,
createInfo: scheduleNextSyncIntervalCreateInfo, createInfo: scheduleNextSyncIntervalCreateInfo,
}), }),
mock<ActiveAlarm>({ mock<ActiveAlarm>({
name: ScheduledTaskNames.fido2ClientAbortTimeout, taskName: ScheduledTaskNames.fido2ClientAbortTimeout,
startTime: Date.now() - 60001, startTime: Date.now() - 60001,
createInfo: { delayInMinutes: 1, periodInMinutes: undefined }, createInfo: { delayInMinutes: 1, periodInMinutes: undefined },
}), }),
@ -131,7 +131,9 @@ describe("BrowserTaskSchedulerService", () => {
}); });
it("triggers a recovered alarm immediately and skips creating the alarm", async () => { it("triggers a recovered alarm immediately and skips creating the alarm", async () => {
activeAlarms = [mock<ActiveAlarm>({ name: ScheduledTaskNames.loginStrategySessionTimeout })]; activeAlarms = [
mock<ActiveAlarm>({ taskName: ScheduledTaskNames.loginStrategySessionTimeout }),
];
browserTaskSchedulerService["recoveredAlarms"].add( browserTaskSchedulerService["recoveredAlarms"].add(
ScheduledTaskNames.loginStrategySessionTimeout, ScheduledTaskNames.loginStrategySessionTimeout,
); );
@ -234,7 +236,9 @@ describe("BrowserTaskSchedulerService", () => {
it("triggers a recovered alarm before creating the interval alarm", async () => { it("triggers a recovered alarm before creating the interval alarm", async () => {
const periodInMinutes = 4; const periodInMinutes = 4;
activeAlarms = [mock<ActiveAlarm>({ name: ScheduledTaskNames.loginStrategySessionTimeout })]; activeAlarms = [
mock<ActiveAlarm>({ taskName: ScheduledTaskNames.loginStrategySessionTimeout }),
];
browserTaskSchedulerService["recoveredAlarms"].add( browserTaskSchedulerService["recoveredAlarms"].add(
ScheduledTaskNames.loginStrategySessionTimeout, ScheduledTaskNames.loginStrategySessionTimeout,
); );

View File

@ -132,21 +132,26 @@ export class BrowserTaskSchedulerService
/** /**
* Creates a browser extension alarm with the given name and create info. * Creates a browser extension alarm with the given name and create info.
* *
* @param name - The name of the alarm. * @param taskName - The name of the alarm.
* @param createInfo - The alarm create info. * @param createInfo - The alarm create info.
*/ */
private async scheduleAlarm( private async scheduleAlarm(
name: ScheduledTaskName, taskName: ScheduledTaskName,
createInfo: chrome.alarms.AlarmCreateInfo, createInfo: chrome.alarms.AlarmCreateInfo,
): Promise<void> { ): Promise<void> {
const existingAlarm = await this.getAlarm(name); const existingAlarm = await this.getAlarm(taskName);
if (existingAlarm) { if (existingAlarm) {
this.logService.warning(`Alarm ${name} already exists. Skipping creation.`); this.logService.warning(`Alarm ${taskName} already exists. Skipping creation.`);
return; return;
} }
await this.createAlarm(name, createInfo); await this.createAlarm(taskName, createInfo);
await this.setActiveAlarm({ name, startTime: Date.now(), createInfo });
await this.setActiveAlarm({
taskName,
startTime: Date.now(),
createInfo,
});
} }
/** /**
@ -158,8 +163,8 @@ export class BrowserTaskSchedulerService
const activeAlarms = await firstValueFrom(this.activeAlarms$); const activeAlarms = await firstValueFrom(this.activeAlarms$);
for (const alarm of activeAlarms) { for (const alarm of activeAlarms) {
const { name, startTime, createInfo } = alarm; const { taskName, startTime, createInfo } = alarm;
const existingAlarm = await this.getAlarm(name); const existingAlarm = await this.getAlarm(taskName);
if (existingAlarm) { if (existingAlarm) {
continue; continue;
} }
@ -170,11 +175,11 @@ export class BrowserTaskSchedulerService
createInfo.delayInMinutes && createInfo.delayInMinutes &&
startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime; startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime;
if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) { if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) {
this.recoveredAlarms.add(name); this.recoveredAlarms.add(taskName);
continue; continue;
} }
void this.scheduleAlarm(name, createInfo); void this.scheduleAlarm(taskName, createInfo);
} }
// 10 seconds after verifying the alarm state, we should treat any newly created alarms as non-recovered alarms. // 10 seconds after verifying the alarm state, we should treat any newly created alarms as non-recovered alarms.
@ -195,11 +200,11 @@ export class BrowserTaskSchedulerService
/** /**
* Deletes an active alarm from state. * Deletes an active alarm from state.
* *
* @param name - The name of the active alarm to delete. * @param taskName - The name of the active alarm to delete.
*/ */
private async deleteActiveAlarm(name: ScheduledTaskName): Promise<void> { private async deleteActiveAlarm(taskName: ScheduledTaskName): Promise<void> {
const activeAlarms = await firstValueFrom(this.activeAlarms$); const activeAlarms = await firstValueFrom(this.activeAlarms$);
const filteredAlarms = activeAlarms?.filter((alarm) => alarm.name !== name); const filteredAlarms = activeAlarms?.filter((alarm) => alarm.taskName !== taskName);
await this.updateActiveAlarms(filteredAlarms || []); await this.updateActiveAlarms(filteredAlarms || []);
} }
@ -247,13 +252,17 @@ export class BrowserTaskSchedulerService
* Triggers an alarm by calling its handler and * Triggers an alarm by calling its handler and
* deleting it if it is a one-time alarm. * deleting it if it is a one-time alarm.
* *
* @param name - The name of the alarm to trigger. * @param alarmName - The name of the alarm to trigger.
* @param periodInMinutes - The period in minutes of an interval alarm. * @param periodInMinutes - The period in minutes of an interval alarm.
*/ */
protected async triggerTask(name: ScheduledTaskName, periodInMinutes?: number): Promise<void> { protected async triggerTask(
const handler = this.taskHandlers.get(name); alarmName: ScheduledTaskName,
periodInMinutes?: number,
): Promise<void> {
const activeUserAlarmName = await this.getActiveUserAlarmName(alarmName);
const handler = this.taskHandlers.get(activeUserAlarmName);
if (!periodInMinutes) { if (!periodInMinutes) {
await this.deleteActiveAlarm(name); await this.deleteActiveAlarm(alarmName);
} }
if (handler) { if (handler) {
@ -265,14 +274,15 @@ export class BrowserTaskSchedulerService
* Clears a new alarm with the given name and create info. Returns a promise * Clears a new alarm with the given name and create info. Returns a promise
* that indicates when the alarm has been cleared successfully. * that indicates when the alarm has been cleared successfully.
* *
* @param alarmName - The name of the alarm to create. * @param taskName - The name of the alarm to create.
*/ */
clearAlarm(alarmName: string): Promise<boolean> { async clearAlarm(taskName: string): Promise<boolean> {
const activeUserAlarmName = await this.getActiveUserAlarmName(taskName);
if (typeof browser !== "undefined" && browser.alarms) { if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.clear(alarmName); return browser.alarms.clear(activeUserAlarmName);
} }
return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve)); return new Promise((resolve) => chrome.alarms.clear(activeUserAlarmName, resolve));
} }
/** /**
@ -290,28 +300,30 @@ export class BrowserTaskSchedulerService
/** /**
* Creates a new alarm with the given name and create info. * Creates a new alarm with the given name and create info.
* *
* @param name - The name of the alarm to create. * @param taskName - The name of the alarm to create.
* @param createInfo - The creation info for the alarm. * @param createInfo - The creation info for the alarm.
*/ */
async createAlarm(name: string, createInfo: chrome.alarms.AlarmCreateInfo): Promise<void> { async createAlarm(taskName: string, createInfo: chrome.alarms.AlarmCreateInfo): Promise<void> {
const activeUserAlarmName = await this.getActiveUserAlarmName(taskName);
if (typeof browser !== "undefined" && browser.alarms) { if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.create(name, createInfo); return browser.alarms.create(activeUserAlarmName, createInfo);
} }
return new Promise((resolve) => chrome.alarms.create(name, createInfo, resolve)); return new Promise((resolve) => chrome.alarms.create(activeUserAlarmName, createInfo, resolve));
} }
/** /**
* Gets the alarm with the given name. * Gets the alarm with the given name.
* *
* @param alarmName - The name of the alarm to get. * @param taskName - The name of the alarm to get.
*/ */
getAlarm(alarmName: string): Promise<chrome.alarms.Alarm> { async getAlarm(taskName: string): Promise<chrome.alarms.Alarm> {
const activeUserAlarmName = await this.getActiveUserAlarmName(taskName);
if (typeof browser !== "undefined" && browser.alarms) { if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.get(alarmName); return browser.alarms.get(activeUserAlarmName);
} }
return new Promise((resolve) => chrome.alarms.get(alarmName, resolve)); return new Promise((resolve) => chrome.alarms.get(activeUserAlarmName, resolve));
} }
/** /**
@ -324,4 +336,13 @@ export class BrowserTaskSchedulerService
return new Promise((resolve) => chrome.alarms.getAll(resolve)); return new Promise((resolve) => chrome.alarms.getAll(resolve));
} }
protected async getActiveUserAlarmName(taskName: string): Promise<string> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (!activeUserId) {
return taskName;
}
return `${activeUserId}_${taskName}`;
}
} }

View File

@ -17,9 +17,9 @@ export abstract class TaskSchedulerService {
protected stateProvider: StateProvider, protected stateProvider: StateProvider,
) {} ) {}
abstract registerTaskHandler(taskName: ScheduledTaskName, handler: () => void): Promise<void>; abstract registerTaskHandler(taskName: ScheduledTaskName, handler: () => void): void;
abstract unregisterTaskHandler(taskName: ScheduledTaskName): Promise<void>; abstract unregisterTaskHandler(taskName: ScheduledTaskName): void;
abstract setTimeout( abstract setTimeout(
taskName: ScheduledTaskName, taskName: ScheduledTaskName,

View File

@ -1,5 +1,3 @@
import { firstValueFrom } from "rxjs";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { TaskIdentifier, TaskSchedulerService } from "../abstractions/task-scheduler.service"; import { TaskIdentifier, TaskSchedulerService } from "../abstractions/task-scheduler.service";
import { ScheduledTaskName } from "../enums/scheduled-task-name.enum"; import { ScheduledTaskName } from "../enums/scheduled-task-name.enum";
@ -12,23 +10,24 @@ export class DefaultTaskSchedulerService extends TaskSchedulerService {
this.taskHandlers = new Map(); this.taskHandlers = new Map();
} }
async registerTaskHandler(taskName: ScheduledTaskName, handler: () => void): Promise<void> { registerTaskHandler(taskName: ScheduledTaskName, handler: () => void) {
const activeUserTaskName = await this.getActiveUserTaskName(taskName); const existingHandler = this.taskHandlers.get(taskName);
const existingHandler = this.taskHandlers.get(activeUserTaskName);
if (existingHandler) { if (existingHandler) {
this.logService.warning(`Task handler for ${taskName} already exists. Overwriting.`); this.logService.warning(`Task handler for ${taskName} already exists. Overwriting.`);
await this.unregisterTaskHandler(taskName); this.unregisterTaskHandler(taskName);
} }
this.taskHandlers.set(activeUserTaskName, handler); this.taskHandlers.set(taskName, handler);
} }
async unregisterTaskHandler(taskName: ScheduledTaskName): Promise<void> { unregisterTaskHandler(taskName: ScheduledTaskName) {
const activeUserTaskName = await this.getActiveUserTaskName(taskName); this.taskHandlers.delete(taskName);
this.taskHandlers.delete(activeUserTaskName);
} }
protected triggerTask(taskName: ScheduledTaskName, _periodInMinutes?: number): void { protected async triggerTask(
taskName: ScheduledTaskName,
_periodInMinutes?: number,
): Promise<void> {
const handler = this.taskHandlers.get(taskName); const handler = this.taskHandlers.get(taskName);
if (handler) { if (handler) {
handler(); handler();
@ -77,13 +76,4 @@ export class DefaultTaskSchedulerService extends TaskSchedulerService {
globalThis.clearInterval(taskIdentifier.intervalId); globalThis.clearInterval(taskIdentifier.intervalId);
} }
} }
private async getActiveUserTaskName(taskName: ScheduledTaskName): Promise<string> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (!activeUserId) {
return taskName;
}
return `${activeUserId}_${taskName}`;
}
} }