1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-06 05:28:51 +02:00

[PM-6426] Finalizing stepped setInterval implementation

This commit is contained in:
Cesar Gonzalez 2024-05-02 08:53:20 -05:00
parent 182b7619aa
commit e3f5e92163
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
2 changed files with 88 additions and 40 deletions

View File

@ -242,23 +242,57 @@ describe("BrowserTaskSchedulerService", () => {
});
describe("setInterval", () => {
it("uses the global setInterval API if the interval is less than 1000ms", async () => {
const intervalInMs = 999;
jest.spyOn(globalThis, "setInterval");
describe("setting an interval that is less than 1 minute", () => {
const intervalInMs = 10000;
await browserTaskSchedulerService.setInterval(
ScheduledTaskNames.loginStrategySessionTimeout,
intervalInMs,
);
it("sets up stepped alarms that trigger behavior after the first minute of setInterval execution", async () => {
await browserTaskSchedulerService.setInterval(
ScheduledTaskNames.loginStrategySessionTimeout,
intervalInMs,
);
expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), intervalInMs);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`${getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout)}__0`,
{ periodInMinutes: 0.5 },
expect.any(Function),
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`${getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout)}__1`,
{ periodInMinutes: 0.6666666666666666 },
expect.any(Function),
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`${getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout)}__2`,
{ periodInMinutes: 0.8333333333333333 },
expect.any(Function),
);
});
it("sets an interval using the global setInterval API", async () => {
jest.spyOn(globalThis, "setInterval");
await browserTaskSchedulerService.setInterval(
ScheduledTaskNames.loginStrategySessionTimeout,
intervalInMs,
);
expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), intervalInMs);
});
it("clears the global setInterval instance once the interval has elapsed the minimum required delay for an alarm", async () => {
jest.useFakeTimers();
jest.spyOn(globalThis, "clearInterval");
await browserTaskSchedulerService.setInterval(
ScheduledTaskNames.loginStrategySessionTimeout,
intervalInMs,
);
jest.advanceTimersByTime(50000);
expect(globalThis.clearInterval).toHaveBeenCalledWith(expect.any(Number));
});
});
it.todo(
"sets up stepped alarms that trigger behavior after the first minute of setInterval execution",
() => {},
);
it("creates an interval alarm", async () => {
const periodInMinutes = 2;
const initialDelayInMs = 1000;

View File

@ -93,7 +93,7 @@ export class BrowserTaskSchedulerServiceImplementation
: intervalInMinutes;
if (intervalInMinutes < 1) {
return this.setupSteppedIntervalAlarms(taskName, alarmName, intervalInMinutes, intervalInMs);
return this.setupSteppedIntervalAlarms(taskName, alarmName, intervalInMs);
}
await this.scheduleAlarm(alarmName, {
@ -102,35 +102,51 @@ export class BrowserTaskSchedulerServiceImplementation
});
}
/**
* Used in cases where the interval is less than 1 minute. This method will set up a setInterval
* to initialize expected recurring behavior, then create a series of alarms to handle the
* expected scheduled task through the alarms api. This is necessary because the alarms
* api does not support intervals less than 1 minute.
*
* @param taskName - The name of the task, separate of any user id.
* @param alarmName - The name of the alarm to create, could contain a user id.
* @param intervalInMs - The interval in milliseconds.
*/
private async setupSteppedIntervalAlarms(
taskName: ScheduledTaskName,
alarmName: string,
intervalInMinutes: number,
intervalInMs: number,
) {
let elapsedMs = 0;
const intervalId: number | NodeJS.Timeout = globalThis.setInterval(async () => {
elapsedMs += intervalInMs;
const elapsedMinutes = elapsedMs / 1000 / 60;
if (elapsedMinutes >= this.getAlarmMinDelayInMinutes()) {
globalThis.clearInterval(intervalId);
return;
}
await this.triggerTask(alarmName, intervalInMinutes);
}, intervalInMs);
): Promise<number | NodeJS.Timeout> {
const alarmMinDelayInMinutes = this.getAlarmMinDelayInMinutes();
const intervalInMinutes = intervalInMs / 1000 / 60;
const numberOfAlarmsToCreate = Math.ceil(1 / intervalInMinutes);
for (let i = 0; i < numberOfAlarmsToCreate; i++) {
const steppedAlarmName = `${alarmName}__${i}`;
const periodInMinutes = this.getAlarmMinDelayInMinutes() + i * intervalInMinutes;
for (let alarmIndex = 0; alarmIndex < numberOfAlarmsToCreate; alarmIndex++) {
const steppedAlarmName = `${alarmName}__${alarmIndex}`;
const periodInMinutes = alarmMinDelayInMinutes + intervalInMinutes * alarmIndex;
// We need to clear alarms based on the task name as well as the
// user-based alarm name to ensure duplicate alarms are not created.
await this.clearScheduledAlarm(`${taskName}__${alarmIndex}`);
await this.clearScheduledAlarm(steppedAlarmName);
await this.clearScheduledAlarm(`${taskName}__${i}`);
await this.scheduleAlarm(steppedAlarmName, {
periodInMinutes: this.getUpperBoundDelayInMinutes(periodInMinutes),
});
}
let elapsedMs = 0;
const intervalId: number | NodeJS.Timeout = globalThis.setInterval(async () => {
elapsedMs += intervalInMs;
const elapsedMinutes = elapsedMs / 1000 / 60;
if (elapsedMinutes >= alarmMinDelayInMinutes) {
globalThis.clearInterval(intervalId);
return;
}
await this.triggerTask(alarmName, intervalInMinutes);
}, intervalInMs);
return intervalId;
}
@ -353,12 +369,7 @@ export class BrowserTaskSchedulerServiceImplementation
* @param alarmName - The alarm name to parse.
*/
private getTaskFromAlarmName(alarmName: string): ScheduledTaskName {
const activeUserTask = alarmName.split("__")[0] as ScheduledTaskName;
if (activeUserTask) {
return activeUserTask;
}
return alarmName as ScheduledTaskName;
return alarmName.split("__")[0] as ScheduledTaskName;
}
/**
@ -427,14 +438,17 @@ export class BrowserTaskSchedulerServiceImplementation
return typeof browser !== "undefined" && !!browser.alarms;
}
/**
* Gets the minimum delay in minutes for an alarm. This is used to ensure that the
* delay is at least 1 minute in non-Chrome environments. In Chrome environments, the
* delay can be as low as 0.5 minutes.
*/
private getAlarmMinDelayInMinutes(): number {
return this.isNonChromeEnvironment() ? 1 : 0.5;
}
/**
* Gets the upper bound delay in minutes for a given delay in minutes. This is
* used to ensure that the delay is at least 1 minute in non-Chrome environments.
* In Chrome environments, the delay can be as low as 0.5 minutes.
* Gets the upper bound delay in minutes for a given delay in minutes.
*
* @param delayInMinutes - The delay in minutes.
*/