1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-06 09:20:43 +01:00

[PM-6426] Working through the details of the implementation and setting up final refinments

This commit is contained in:
Cesar Gonzalez 2024-04-23 10:52:58 -05:00
parent e7d1769f50
commit f0f4bd5af1
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
10 changed files with 302 additions and 358 deletions

View File

@ -503,14 +503,20 @@ export default class MainBackground {
this.globalStateProvider, this.globalStateProvider,
this.derivedStateProvider, this.derivedStateProvider,
); );
this.taskSchedulerService = new BrowserTaskSchedulerService(
this.logService, // The taskSchedulerService needs to be instantiated a single time in a potential context.
this.stateProvider, // Since the popup creates a new instance of the main background in mv3, we need to guard against a duplicate registration.
); if (!this.popupOnlyContext) {
void this.taskSchedulerService.registerTaskHandler( this.taskSchedulerService = new BrowserTaskSchedulerService(
ScheduledTaskNames.scheduleNextSyncInterval, this.logService,
() => this.fullSync(), this.stateProvider,
); );
void this.taskSchedulerService.registerTaskHandler(
ScheduledTaskNames.scheduleNextSyncInterval,
() => this.fullSync(),
);
}
this.environmentService = new BrowserEnvironmentService( this.environmentService = new BrowserEnvironmentService(
this.logService, this.logService,
this.stateProvider, this.stateProvider,
@ -921,13 +927,13 @@ export default class MainBackground {
this.logService, this.logService,
); );
const systemUtilsServiceReloadCallback = () => { const systemUtilsServiceReloadCallback = async () => {
const forceWindowReload = const forceWindowReload =
this.platformUtilsService.isSafari() || this.platformUtilsService.isSafari() ||
this.platformUtilsService.isFirefox() || this.platformUtilsService.isFirefox() ||
this.platformUtilsService.isOpera(); this.platformUtilsService.isOpera();
await this.taskSchedulerService?.clearAllScheduledTasks();
BrowserApi.reloadExtension(forceWindowReload ? self : null); BrowserApi.reloadExtension(forceWindowReload ? self : null);
return Promise.resolve();
}; };
this.systemService = new SystemService( this.systemService = new SystemService(
@ -1162,12 +1168,12 @@ export default class MainBackground {
} }
await this.fullSync(true); await this.fullSync(true);
await this.taskSchedulerService.setInterval( await this.taskSchedulerService?.setInterval(
ScheduledTaskNames.scheduleNextSyncInterval, ScheduledTaskNames.scheduleNextSyncInterval,
5 * 60 * 1000, // check every 5 minutes 5 * 60 * 1000, // check every 5 minutes
); );
setTimeout(() => this.notificationsService.init(), 2500); setTimeout(() => this.notificationsService.init(), 2500);
await this.taskSchedulerService.verifyAlarmsState(); await this.taskSchedulerService?.verifyAlarmsState();
resolve(); resolve();
}, 500); }, 500);
}); });
@ -1244,7 +1250,6 @@ export default class MainBackground {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.eventUploadService.uploadEvents(userId as UserId); await this.eventUploadService.uploadEvents(userId as UserId);
await this.taskSchedulerService.clearAllScheduledTasks();
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0), userId), this.syncService.setLastSync(new Date(0), userId),

View File

@ -60,7 +60,8 @@
"clipboardWrite", "clipboardWrite",
"idle", "idle",
"scripting", "scripting",
"offscreen" "offscreen",
"alarms"
], ],
"optional_permissions": ["nativeMessaging", "privacy"], "optional_permissions": ["nativeMessaging", "privacy"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],

View File

@ -1,8 +1,7 @@
import { TaskSchedulerService } from "@bitwarden/common/platform/abstractions/task-scheduler.service"; import { TaskSchedulerService } from "@bitwarden/common/platform/abstractions/task-scheduler.service";
import { ScheduledTaskName } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
export type ActiveAlarm = { export type ActiveAlarm = {
taskName: ScheduledTaskName; alarmName: string;
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>({
taskName: ScheduledTaskNames.eventUploadsInterval, alarmName: ScheduledTaskNames.eventUploadsInterval,
createInfo: eventUploadsIntervalCreateInfo, createInfo: eventUploadsIntervalCreateInfo,
}), }),
mock<ActiveAlarm>({ mock<ActiveAlarm>({
taskName: ScheduledTaskNames.scheduleNextSyncInterval, alarmName: ScheduledTaskNames.scheduleNextSyncInterval,
createInfo: scheduleNextSyncIntervalCreateInfo, createInfo: scheduleNextSyncIntervalCreateInfo,
}), }),
mock<ActiveAlarm>({ mock<ActiveAlarm>({
taskName: ScheduledTaskNames.fido2ClientAbortTimeout, alarmName: ScheduledTaskNames.fido2ClientAbortTimeout,
startTime: Date.now() - 60001, startTime: Date.now() - 60001,
createInfo: { delayInMinutes: 1, periodInMinutes: undefined }, createInfo: { delayInMinutes: 1, periodInMinutes: undefined },
}), }),
@ -87,24 +87,6 @@ describe("BrowserTaskSchedulerService", () => {
expect.any(Function), expect.any(Function),
); );
}); });
it("adds the alarm name to the set of recovered alarms if the alarm create info indicates it has expired", () => {
expect(
browserTaskSchedulerService["recoveredAlarms"].has(
ScheduledTaskNames.fido2ClientAbortTimeout,
),
).toBe(true);
});
it("clears the list of recovered alarms after 10 seconds", () => {
jest.advanceTimersByTime(10 * 1000);
expect(
browserTaskSchedulerService["recoveredAlarms"].has(
ScheduledTaskNames.fido2ClientAbortTimeout,
),
).toBe(false);
});
}); });
describe("setTimeout", () => { describe("setTimeout", () => {
@ -130,32 +112,6 @@ describe("BrowserTaskSchedulerService", () => {
); );
}); });
it("triggers a recovered alarm immediately and skips creating the alarm", async () => {
activeAlarms = [
mock<ActiveAlarm>({ taskName: ScheduledTaskNames.loginStrategySessionTimeout }),
];
browserTaskSchedulerService["recoveredAlarms"].add(
ScheduledTaskNames.loginStrategySessionTimeout,
);
const callback = jest.fn();
await browserTaskSchedulerService.registerTaskHandler(
ScheduledTaskNames.loginStrategySessionTimeout,
callback,
);
await browserTaskSchedulerService.setTimeout(
ScheduledTaskNames.loginStrategySessionTimeout,
60 * 1000,
);
expect(callback).toHaveBeenCalled();
expect(chrome.alarms.create).not.toHaveBeenCalledWith(
ScheduledTaskNames.loginStrategySessionTimeout,
{ delayInMinutes: 1 },
expect.any(Function),
);
});
it("creates a timeout alarm", async () => { it("creates a timeout alarm", async () => {
const callback = jest.fn(); const callback = jest.fn();
const delayInMinutes = 2; const delayInMinutes = 2;
@ -176,27 +132,27 @@ describe("BrowserTaskSchedulerService", () => {
); );
}); });
it("skips creating a duplicate timeout alarm", async () => { // it("skips creating a duplicate timeout alarm", async () => {
const callback = jest.fn(); // const callback = jest.fn();
const delayInMinutes = 2; // const delayInMinutes = 2;
jest.spyOn(browserTaskSchedulerService as any, "getAlarm").mockResolvedValue( // jest.spyOn(browserTaskSchedulerService as any, "getAlarm").mockResolvedValue(
mock<chrome.alarms.Alarm>({ // mock<chrome.alarms.Alarm>({
name: ScheduledTaskNames.loginStrategySessionTimeout, // name: ScheduledTaskNames.loginStrategySessionTimeout,
}), // }),
); // );
jest.spyOn(browserTaskSchedulerService, "createAlarm"); // jest.spyOn(browserTaskSchedulerService, "createAlarm");
await browserTaskSchedulerService.registerTaskHandler( // await browserTaskSchedulerService.registerTaskHandler(
ScheduledTaskNames.loginStrategySessionTimeout, // ScheduledTaskNames.loginStrategySessionTimeout,
callback, // callback,
); // );
//
await browserTaskSchedulerService.setTimeout( // await browserTaskSchedulerService.setTimeout(
ScheduledTaskNames.loginStrategySessionTimeout, // ScheduledTaskNames.loginStrategySessionTimeout,
delayInMinutes * 60 * 1000, // delayInMinutes * 60 * 1000,
); // );
//
expect(browserTaskSchedulerService.createAlarm).not.toHaveBeenCalled(); // expect(browserTaskSchedulerService.createAlarm).not.toHaveBeenCalled();
}); // });
// it("logs a warning if a duplicate handler is registered when creating an alarm", () => { // it("logs a warning if a duplicate handler is registered when creating an alarm", () => {
// const callback = jest.fn(); // const callback = jest.fn();
@ -234,33 +190,6 @@ describe("BrowserTaskSchedulerService", () => {
); );
}); });
it("triggers a recovered alarm before creating the interval alarm", async () => {
const periodInMinutes = 4;
activeAlarms = [
mock<ActiveAlarm>({ taskName: ScheduledTaskNames.loginStrategySessionTimeout }),
];
browserTaskSchedulerService["recoveredAlarms"].add(
ScheduledTaskNames.loginStrategySessionTimeout,
);
const callback = jest.fn();
await browserTaskSchedulerService.registerTaskHandler(
ScheduledTaskNames.loginStrategySessionTimeout,
callback,
);
await browserTaskSchedulerService.setInterval(
ScheduledTaskNames.loginStrategySessionTimeout,
periodInMinutes * 60 * 1000,
);
expect(callback).toHaveBeenCalled();
expect(chrome.alarms.create).toHaveBeenCalledWith(
ScheduledTaskNames.loginStrategySessionTimeout,
{ periodInMinutes, delayInMinutes: periodInMinutes },
expect.any(Function),
);
});
it("creates an interval alarm", async () => { it("creates an interval alarm", async () => {
const callback = jest.fn(); const callback = jest.fn();
const periodInMinutes = 2; const periodInMinutes = 2;
@ -324,19 +253,19 @@ describe("BrowserTaskSchedulerService", () => {
}); });
}); });
describe("clearAllScheduledTasks", () => { // describe("clearAllScheduledTasks", () => {
it("clears all scheduled tasks and extension alarms", async () => { // it("clears all scheduled tasks and extension alarms", async () => {
jest.spyOn(browserTaskSchedulerService, "clearAllAlarms"); // jest.spyOn(browserTaskSchedulerService, "clearAllAlarms");
jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms"); // jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms");
//
await browserTaskSchedulerService.clearAllScheduledTasks(); // await browserTaskSchedulerService.clearAllScheduledTasks();
//
expect(browserTaskSchedulerService.clearAllAlarms).toHaveBeenCalled(); // expect(browserTaskSchedulerService.clearAllAlarms).toHaveBeenCalled();
expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]); // expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]);
// expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({}); // // expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({});
expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0); // expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0);
}); // });
}); // });
// describe("handleOnAlarm", () => { // describe("handleOnAlarm", () => {
// it("triggers the alarm", async () => { // it("triggers the alarm", async () => {
@ -350,133 +279,133 @@ describe("BrowserTaskSchedulerService", () => {
// }); // });
// }); // });
describe("clearAlarm", () => { // describe("clearAlarm", () => {
it("uses the browser.alarms API if it is available", async () => { // it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name"; // const alarmName = "alarm-name";
globalThis.browser = { // globalThis.browser = {
// eslint-disable-next-line // // eslint-disable-next-line
// @ts-ignore // // @ts-ignore
alarms: { // alarms: {
clear: jest.fn(), // clear: jest.fn(),
}, // },
}; // };
//
await browserTaskSchedulerService.clearAlarm(alarmName); // await browserTaskSchedulerService.clearAlarm(alarmName);
//
expect(browser.alarms.clear).toHaveBeenCalledWith(alarmName); // expect(browser.alarms.clear).toHaveBeenCalledWith(alarmName);
}); // });
//
it("clears the alarm with the provided name", async () => { // it("clears the alarm with the provided name", async () => {
const alarmName = "alarm-name"; // const alarmName = "alarm-name";
//
const wasCleared = await browserTaskSchedulerService.clearAlarm(alarmName); // const wasCleared = await browserTaskSchedulerService.clearAlarm(alarmName);
//
expect(chrome.alarms.clear).toHaveBeenCalledWith(alarmName, expect.any(Function)); // expect(chrome.alarms.clear).toHaveBeenCalledWith(alarmName, expect.any(Function));
expect(wasCleared).toBe(true); // expect(wasCleared).toBe(true);
}); // });
}); // });
//
describe("clearAllAlarms", () => { // describe("clearAllAlarms", () => {
it("uses the browser.alarms API if it is available", async () => { // it("uses the browser.alarms API if it is available", async () => {
globalThis.browser = { // globalThis.browser = {
// eslint-disable-next-line // // eslint-disable-next-line
// @ts-ignore // // @ts-ignore
alarms: { // alarms: {
clearAll: jest.fn(), // clearAll: jest.fn(),
}, // },
}; // };
//
await browserTaskSchedulerService.clearAllAlarms(); // await browserTaskSchedulerService.clearAllAlarms();
//
expect(browser.alarms.clearAll).toHaveBeenCalled(); // expect(browser.alarms.clearAll).toHaveBeenCalled();
}); // });
//
it("clears all alarms", async () => { // it("clears all alarms", async () => {
const wasCleared = await browserTaskSchedulerService.clearAllAlarms(); // const wasCleared = await browserTaskSchedulerService.clearAllAlarms();
//
expect(chrome.alarms.clearAll).toHaveBeenCalledWith(expect.any(Function)); // expect(chrome.alarms.clearAll).toHaveBeenCalledWith(expect.any(Function));
expect(wasCleared).toBe(true); // expect(wasCleared).toBe(true);
}); // });
}); // });
//
describe("createAlarm", () => { // describe("createAlarm", () => {
it("uses the browser.alarms API if it is available", async () => { // it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name"; // const alarmName = "alarm-name";
const alarmInfo = { when: 1000 }; // const alarmInfo = { when: 1000 };
globalThis.browser = { // globalThis.browser = {
// eslint-disable-next-line // // eslint-disable-next-line
// @ts-ignore // // @ts-ignore
alarms: { // alarms: {
create: jest.fn(), // create: jest.fn(),
}, // },
}; // };
//
await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo); // await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo);
//
expect(browser.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo); // expect(browser.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo);
}); // });
//
it("creates an alarm", async () => { // it("creates an alarm", async () => {
const alarmName = "alarm-name"; // const alarmName = "alarm-name";
const alarmInfo = { when: 1000 }; // const alarmInfo = { when: 1000 };
//
await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo); // await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo);
//
expect(chrome.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo, expect.any(Function)); // expect(chrome.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo, expect.any(Function));
}); // });
}); // });
//
describe.skip("getAlarm", () => { // describe.skip("getAlarm", () => {
it("uses the browser.alarms API if it is available", async () => { // it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name"; // const alarmName = "alarm-name";
globalThis.browser = { // globalThis.browser = {
// eslint-disable-next-line // // eslint-disable-next-line
// @ts-ignore // // @ts-ignore
alarms: { // alarms: {
get: jest.fn(), // get: jest.fn(),
}, // },
}; // };
//
await browserTaskSchedulerService.getAlarm(alarmName); // await browserTaskSchedulerService.getAlarm(alarmName);
//
expect(browser.alarms.get).toHaveBeenCalledWith(alarmName); // expect(browser.alarms.get).toHaveBeenCalledWith(alarmName);
}); // });
//
it("gets the alarm by name", async () => { // it("gets the alarm by name", async () => {
const alarmName = "alarm-name"; // const alarmName = "alarm-name";
const alarmMock = mock<chrome.alarms.Alarm>(); // const alarmMock = mock<chrome.alarms.Alarm>();
chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(alarmMock)); // chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(alarmMock));
//
const receivedAlarm = await browserTaskSchedulerService.getAlarm(alarmName); // const receivedAlarm = await browserTaskSchedulerService.getAlarm(alarmName);
//
expect(chrome.alarms.get).toHaveBeenCalledWith(alarmName, expect.any(Function)); // expect(chrome.alarms.get).toHaveBeenCalledWith(alarmName, expect.any(Function));
expect(receivedAlarm).toBe(alarmMock); // expect(receivedAlarm).toBe(alarmMock);
}); // });
}); // });
//
describe("getAllAlarms", () => { // describe("getAllAlarms", () => {
it("uses the browser.alarms API if it is available", async () => { // it("uses the browser.alarms API if it is available", async () => {
globalThis.browser = { // globalThis.browser = {
// eslint-disable-next-line // // eslint-disable-next-line
// @ts-ignore // // @ts-ignore
alarms: { // alarms: {
getAll: jest.fn(), // getAll: jest.fn(),
}, // },
}; // };
//
await browserTaskSchedulerService.getAllAlarms(); // await browserTaskSchedulerService.getAllAlarms();
//
expect(browser.alarms.getAll).toHaveBeenCalled(); // expect(browser.alarms.getAll).toHaveBeenCalled();
}); // });
//
it("gets all registered alarms", async () => { // it("gets all registered alarms", async () => {
const alarms = [mock<chrome.alarms.Alarm>(), mock<chrome.alarms.Alarm>()]; // const alarms = [mock<chrome.alarms.Alarm>(), mock<chrome.alarms.Alarm>()];
chrome.alarms.getAll = jest.fn().mockImplementation((callback) => callback(alarms)); // chrome.alarms.getAll = jest.fn().mockImplementation((callback) => callback(alarms));
//
const receivedAlarms = await browserTaskSchedulerService.getAllAlarms(); // const receivedAlarms = await browserTaskSchedulerService.getAllAlarms();
//
expect(chrome.alarms.getAll).toHaveBeenCalledWith(expect.any(Function)); // expect(chrome.alarms.getAll).toHaveBeenCalledWith(expect.any(Function));
expect(receivedAlarms).toBe(alarms); // expect(receivedAlarms).toBe(alarms);
}); // });
}); // });
}); });

View File

@ -28,7 +28,6 @@ export class BrowserTaskSchedulerService
{ {
private activeAlarmsState: GlobalState<ActiveAlarm[]>; private activeAlarmsState: GlobalState<ActiveAlarm[]>;
readonly activeAlarms$: Observable<ActiveAlarm[]>; readonly activeAlarms$: Observable<ActiveAlarm[]>;
private recoveredAlarms: Set<string> = new Set();
constructor(logService: LogService, stateProvider: StateProvider) { constructor(logService: LogService, stateProvider: StateProvider) {
super(logService, stateProvider); super(logService, stateProvider);
@ -58,12 +57,8 @@ export class BrowserTaskSchedulerService
return super.setTimeout(taskName, delayInMs); return super.setTimeout(taskName, delayInMs);
} }
if (this.recoveredAlarms.has(taskName)) { const alarmName = await this.getActiveUserAlarmName(taskName);
await this.triggerRecoveredAlarm(taskName); await this.scheduleAlarm(alarmName, { delayInMinutes });
return;
}
await this.scheduleAlarm(taskName, { delayInMinutes });
} }
/** /**
@ -85,12 +80,9 @@ export class BrowserTaskSchedulerService
return super.setInterval(taskName, intervalInMs); return super.setInterval(taskName, intervalInMs);
} }
if (this.recoveredAlarms.has(taskName)) { const alarmName = await this.getActiveUserAlarmName(taskName);
await this.triggerRecoveredAlarm(taskName, intervalInMinutes);
}
const initialDelayInMinutes = initialDelayInMs ? initialDelayInMs / 1000 / 60 : undefined; const initialDelayInMinutes = initialDelayInMs ? initialDelayInMs / 1000 / 60 : undefined;
await this.scheduleAlarm(taskName, { await this.scheduleAlarm(alarmName, {
periodInMinutes: intervalInMinutes, periodInMinutes: intervalInMinutes,
delayInMinutes: initialDelayInMinutes ?? intervalInMinutes, delayInMinutes: initialDelayInMinutes ?? intervalInMinutes,
}); });
@ -111,10 +103,10 @@ export class BrowserTaskSchedulerService
return; return;
} }
const wasCleared = await this.clearAlarm(taskName); const alarmName = await this.getActiveUserAlarmName(taskName);
const wasCleared = await this.clearAlarm(alarmName);
if (wasCleared) { if (wasCleared) {
await this.deleteActiveAlarm(taskName); await this.deleteActiveAlarm(alarmName);
this.recoveredAlarms.delete(taskName);
} }
} }
@ -125,7 +117,6 @@ export class BrowserTaskSchedulerService
async clearAllScheduledTasks(): Promise<void> { async clearAllScheduledTasks(): Promise<void> {
await this.clearAllAlarms(); await this.clearAllAlarms();
await this.updateActiveAlarms([]); await this.updateActiveAlarms([]);
this.recoveredAlarms.clear();
} }
/** /**
@ -137,8 +128,12 @@ 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 { taskName, startTime, createInfo } = alarm; const { alarmName, startTime, createInfo } = alarm;
const existingAlarm = await this.getAlarm(taskName); if (!alarmName) {
return;
}
const existingAlarm = await this.getAlarm(alarmName);
if (existingAlarm) { if (existingAlarm) {
continue; continue;
} }
@ -149,61 +144,72 @@ 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(taskName); await this.triggerRecoveredAlarm(alarmName);
continue; continue;
} }
void this.scheduleAlarm(taskName, createInfo); void this.scheduleAlarm(alarmName, createInfo);
} }
// 10 seconds after verifying the alarm state, we should treat any newly created alarms as non-recovered alarms.
globalThis.setTimeout(() => this.recoveredAlarms.clear(), 10 * 1000);
} }
/** /**
* 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 taskName - The name of the alarm. * @param alarmName - The name of the alarm.
* @param createInfo - The alarm create info. * @param createInfo - The alarm create info.
*/ */
private async scheduleAlarm( private async scheduleAlarm(
taskName: ScheduledTaskName, alarmName: string,
createInfo: chrome.alarms.AlarmCreateInfo, createInfo: chrome.alarms.AlarmCreateInfo,
): Promise<void> { ): Promise<void> {
const existingAlarm = await this.getAlarm(taskName); if (!alarmName) {
if (existingAlarm) {
this.logService.warning(`Alarm ${taskName} already exists. Skipping creation.`);
return; return;
} }
await this.createAlarm(taskName, createInfo); const existingAlarm = await this.getAlarm(alarmName);
if (existingAlarm) {
this.logService.warning(`Alarm ${alarmName} already exists. Skipping creation.`);
return;
}
await this.setActiveAlarm({ const taskName = this.getTaskFromAlarmName(alarmName);
taskName, const existingTaskBasedAlarm = await this.getAlarm(taskName);
startTime: Date.now(), if (existingTaskBasedAlarm) {
createInfo, await this.clearAlarm(taskName);
}); }
await this.createAlarm(alarmName, createInfo);
await this.setActiveAlarm(alarmName, createInfo);
} }
/** /**
* Sets an active alarm in state. * Sets an active alarm in state.
* *
* @param alarm - The active alarm to set. * @param alarmName - The name of the active alarm to set.
* @param createInfo - The creation info of the active alarm.
*/ */
private async setActiveAlarm(alarm: ActiveAlarm): Promise<void> { private async setActiveAlarm(
alarmName: string,
createInfo: chrome.alarms.AlarmCreateInfo,
): Promise<void> {
const activeAlarms = await firstValueFrom(this.activeAlarms$); const activeAlarms = await firstValueFrom(this.activeAlarms$);
activeAlarms.push(alarm); const filteredAlarms = activeAlarms?.filter((alarm) => alarm.alarmName !== alarmName);
await this.updateActiveAlarms(activeAlarms); filteredAlarms.push({
alarmName,
startTime: Date.now(),
createInfo,
});
await this.updateActiveAlarms(filteredAlarms);
} }
/** /**
* Deletes an active alarm from state. * Deletes an active alarm from state.
* *
* @param taskName - The name of the active alarm to delete. * @param alarmName - The name of the active alarm to delete.
*/ */
private async deleteActiveAlarm(taskName: ScheduledTaskName): Promise<void> { private async deleteActiveAlarm(alarmName: string): Promise<void> {
const activeAlarms = await firstValueFrom(this.activeAlarms$); const activeAlarms = await firstValueFrom(this.activeAlarms$);
const filteredAlarms = activeAlarms?.filter((alarm) => alarm.taskName !== taskName); const filteredAlarms = activeAlarms?.filter((alarm) => alarm.alarmName !== alarmName);
await this.updateActiveAlarms(filteredAlarms || []); await this.updateActiveAlarms(filteredAlarms || []);
} }
@ -219,15 +225,11 @@ export class BrowserTaskSchedulerService
/** /**
* Triggers a recovered alarm by deleting it from the recovered alarms set * Triggers a recovered alarm by deleting it from the recovered alarms set
* *
* @param name - The name of the recovered alarm to trigger. * @param alarmName - The name of the recovered alarm to trigger.
* @param periodInMinutes - The period in minutes of the recovered alarm. * @param periodInMinutes - The period in minutes of the recovered alarm.
*/ */
private async triggerRecoveredAlarm( private async triggerRecoveredAlarm(alarmName: string, periodInMinutes?: number): Promise<void> {
name: ScheduledTaskName, await this.triggerTask(alarmName, periodInMinutes);
periodInMinutes?: number,
): Promise<void> {
this.recoveredAlarms.delete(name);
await this.triggerTask(name, periodInMinutes);
} }
/** /**
@ -244,7 +246,7 @@ export class BrowserTaskSchedulerService
*/ */
private handleOnAlarm = async (alarm: chrome.alarms.Alarm): Promise<void> => { private handleOnAlarm = async (alarm: chrome.alarms.Alarm): Promise<void> => {
const { name, periodInMinutes } = alarm; const { name, periodInMinutes } = alarm;
await this.triggerTask(name as ScheduledTaskName, periodInMinutes); await this.triggerTask(name, periodInMinutes);
}; };
/** /**
@ -254,12 +256,9 @@ export class BrowserTaskSchedulerService
* @param alarmName - 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( protected async triggerTask(alarmName: string, periodInMinutes?: number): Promise<void> {
alarmName: ScheduledTaskName, const taskName = this.getTaskFromAlarmName(alarmName);
periodInMinutes?: number, const handler = this.taskHandlers.get(taskName);
): Promise<void> {
const activeUserAlarmName = this.getTaskFromAlarmName(alarmName);
const handler = this.taskHandlers.get(activeUserAlarmName);
if (!periodInMinutes) { if (!periodInMinutes) {
await this.deleteActiveAlarm(alarmName); await this.deleteActiveAlarm(alarmName);
} }
@ -267,28 +266,44 @@ export class BrowserTaskSchedulerService
if (handler) { if (handler) {
handler(); handler();
} }
if (alarmName === taskName) {
await this.verifyNonUserBasedAlarm(taskName);
}
}
private async verifyNonUserBasedAlarm(alarmName: ScheduledTaskName): Promise<void> {
const activeUserAlarm = await this.getActiveUserAlarmName(alarmName);
const existingAlarm = await this.getAlarm(activeUserAlarm);
if (!existingAlarm) {
return;
}
const wasCleared = await this.clearAlarm(alarmName);
if (wasCleared) {
await this.deleteActiveAlarm(alarmName);
}
} }
/** /**
* 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 taskName - The name of the alarm to create. * @param alarmName - The name of the alarm to create.
*/ */
async clearAlarm(taskName: string): Promise<boolean> { private async clearAlarm(alarmName: 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(activeUserAlarmName); return browser.alarms.clear(alarmName);
} }
return new Promise((resolve) => chrome.alarms.clear(activeUserAlarmName, resolve)); return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve));
} }
/** /**
* Clears all alarms that have been set by the extension. Returns a promise * Clears all alarms that have been set by the extension. Returns a promise
* that indicates when all alarms have been cleared successfully. * that indicates when all alarms have been cleared successfully.
*/ */
clearAllAlarms(): Promise<boolean> { private clearAllAlarms(): Promise<boolean> {
if (typeof browser !== "undefined" && browser.alarms) { if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.clearAll(); return browser.alarms.clearAll();
} }
@ -299,44 +314,34 @@ 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 taskName - The name of the alarm to create. * @param alarmName - 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(taskName: string, createInfo: chrome.alarms.AlarmCreateInfo): Promise<void> { private async createAlarm(
const activeUserAlarmName = await this.getActiveUserAlarmName(taskName); alarmName: string,
createInfo: chrome.alarms.AlarmCreateInfo,
): Promise<void> {
if (typeof browser !== "undefined" && browser.alarms) { if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.create(activeUserAlarmName, createInfo); return browser.alarms.create(alarmName, createInfo);
} }
return new Promise((resolve) => chrome.alarms.create(activeUserAlarmName, createInfo, resolve)); return new Promise((resolve) => chrome.alarms.create(alarmName, createInfo, resolve));
} }
/** /**
* Gets the alarm with the given name. * Gets the alarm with the given name.
* *
* @param taskName - The name of the alarm to get. * @param alarmName - The name of the alarm to get.
*/ */
async getAlarm(taskName: string): Promise<chrome.alarms.Alarm> { private async getAlarm(alarmName: 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(activeUserAlarmName); return browser.alarms.get(alarmName);
} }
return new Promise((resolve) => chrome.alarms.get(activeUserAlarmName, resolve)); return new Promise((resolve) => chrome.alarms.get(alarmName, resolve));
} }
/** private async getActiveUserAlarmName(taskName: ScheduledTaskName): Promise<string> {
* Gets all alarms that have been set by the extension.
*/
getAllAlarms(): Promise<chrome.alarms.Alarm[]> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.getAll();
}
return new Promise((resolve) => chrome.alarms.getAll(resolve));
}
protected async getActiveUserAlarmName(taskName: string): Promise<string> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (!activeUserId) { if (!activeUserId) {
return taskName; return taskName;
@ -345,7 +350,12 @@ export class BrowserTaskSchedulerService
return `${activeUserId}__${taskName}`; return `${activeUserId}__${taskName}`;
} }
private getTaskFromAlarmName(alarmName: string): string { private getTaskFromAlarmName(alarmName: string): ScheduledTaskName {
return alarmName.split("__")[1]; const activeUserTask = alarmName.split("__")[1] as ScheduledTaskName;
if (activeUserTask) {
return activeUserTask;
}
return alarmName as ScheduledTaskName;
} }
} }

View File

@ -107,7 +107,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected stateProvider: GlobalStateProvider, protected stateProvider: GlobalStateProvider,
protected billingAccountProfileStateService: BillingAccountProfileStateService, protected billingAccountProfileStateService: BillingAccountProfileStateService,
protected taskSchedulerService: TaskSchedulerService, protected taskSchedulerService?: TaskSchedulerService,
) { ) {
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
@ -115,7 +115,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.authRequestPushNotificationState = this.stateProvider.get( this.authRequestPushNotificationState = this.stateProvider.get(
AUTH_REQUEST_PUSH_NOTIFICATION_KEY, AUTH_REQUEST_PUSH_NOTIFICATION_KEY,
); );
void this.taskSchedulerService.registerTaskHandler( void this.taskSchedulerService?.registerTaskHandler(
ScheduledTaskNames.loginStrategySessionTimeout, ScheduledTaskNames.loginStrategySessionTimeout,
() => this.clearCache(), () => this.clearCache(),
); );
@ -312,7 +312,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
await this.loginStrategyCacheExpirationState.update( await this.loginStrategyCacheExpirationState.update(
(_) => new Date(Date.now() + sessionTimeoutLength), (_) => new Date(Date.now() + sessionTimeoutLength),
); );
this.sessionTimeout = await this.taskSchedulerService.setTimeout( this.sessionTimeout = await this.taskSchedulerService?.setTimeout(
ScheduledTaskNames.loginStrategySessionTimeout, ScheduledTaskNames.loginStrategySessionTimeout,
sessionTimeoutLength, sessionTimeoutLength,
); );
@ -320,7 +320,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
private async clearSessionTimeout(): Promise<void> { private async clearSessionTimeout(): Promise<void> {
await this.loginStrategyCacheExpirationState.update((_) => null); await this.loginStrategyCacheExpirationState.update((_) => null);
await this.taskSchedulerService.clearScheduledTask({ await this.taskSchedulerService?.clearScheduledTask({
taskName: ScheduledTaskNames.loginStrategySessionTimeout, taskName: ScheduledTaskNames.loginStrategySessionTimeout,
timeoutId: this.sessionTimeout, timeoutId: this.sessionTimeout,
}); });

View File

@ -27,9 +27,9 @@ export class SystemService implements SystemServiceAbstraction {
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private taskSchedulerService: TaskSchedulerService, private taskSchedulerService?: TaskSchedulerService,
) { ) {
void this.taskSchedulerService.registerTaskHandler( void this.taskSchedulerService?.registerTaskHandler(
ScheduledTaskNames.systemClearClipboardTimeout, ScheduledTaskNames.systemClearClipboardTimeout,
() => this.clearPendingClipboard(), () => this.clearPendingClipboard(),
); );
@ -102,7 +102,7 @@ export class SystemService implements SystemServiceAbstraction {
} }
async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> { async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> {
await this.taskSchedulerService.clearScheduledTask({ await this.taskSchedulerService?.clearScheduledTask({
taskName: ScheduledTaskNames.systemClearClipboardTimeout, taskName: ScheduledTaskNames.systemClearClipboardTimeout,
timeoutId: this.clearClipboardTimeout, timeoutId: this.clearClipboardTimeout,
}); });
@ -130,7 +130,7 @@ export class SystemService implements SystemServiceAbstraction {
} }
}; };
this.clearClipboardTimeout = this.taskSchedulerService.setTimeout( this.clearClipboardTimeout = this.taskSchedulerService?.setTimeout(
ScheduledTaskNames.systemClearClipboardTimeout, ScheduledTaskNames.systemClearClipboardTimeout,
taskTimeoutInMs, taskTimeoutInMs,
); );

View File

@ -21,9 +21,9 @@ export class EventUploadService implements EventUploadServiceAbstraction {
private stateProvider: StateProvider, private stateProvider: StateProvider,
private logService: LogService, private logService: LogService,
private authService: AuthService, private authService: AuthService,
private taskSchedulerService: TaskSchedulerService, private taskSchedulerService?: TaskSchedulerService,
) { ) {
void this.taskSchedulerService.registerTaskHandler( void this.taskSchedulerService?.registerTaskHandler(
ScheduledTaskNames.eventUploadsInterval, ScheduledTaskNames.eventUploadsInterval,
() => this.uploadEvents(), () => this.uploadEvents(),
); );
@ -37,7 +37,7 @@ export class EventUploadService implements EventUploadServiceAbstraction {
this.inited = true; this.inited = true;
if (checkOnInterval) { if (checkOnInterval) {
void this.uploadEvents(); void this.uploadEvents();
void this.taskSchedulerService.setInterval( void this.taskSchedulerService?.setInterval(
ScheduledTaskNames.eventUploadsInterval, ScheduledTaskNames.eventUploadsInterval,
60 * 1000, // check every 60 seconds 60 * 1000, // check every 60 seconds
); );

View File

@ -44,9 +44,9 @@ export class NotificationsService implements NotificationsServiceAbstraction {
private authService: AuthService, private authService: AuthService,
private authRequestService: AuthRequestServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction,
private messagingService: MessagingService, private messagingService: MessagingService,
private taskSchedulerService: TaskSchedulerService, private taskSchedulerService?: TaskSchedulerService,
) { ) {
void this.taskSchedulerService.registerTaskHandler( void this.taskSchedulerService?.registerTaskHandler(
ScheduledTaskNames.notificationsReconnectTimeout, ScheduledTaskNames.notificationsReconnectTimeout,
() => this.reconnect(this.isSyncingOnReconnect), () => this.reconnect(this.isSyncingOnReconnect),
); );
@ -225,7 +225,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
} }
private async reconnect(sync: boolean) { private async reconnect(sync: boolean) {
await this.taskSchedulerService.clearScheduledTask({ await this.taskSchedulerService?.clearScheduledTask({
taskName: ScheduledTaskNames.notificationsReconnectTimeout, taskName: ScheduledTaskNames.notificationsReconnectTimeout,
timeoutId: this.reconnectTimer, timeoutId: this.reconnectTimer,
}); });
@ -250,7 +250,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
if (!this.connected) { if (!this.connected) {
this.isSyncingOnReconnect = sync; this.isSyncingOnReconnect = sync;
this.reconnectTimer = await this.taskSchedulerService.setTimeout( this.reconnectTimer = await this.taskSchedulerService?.setTimeout(
ScheduledTaskNames.notificationsReconnectTimeout, ScheduledTaskNames.notificationsReconnectTimeout,
this.random(120000, 300000), this.random(120000, 300000),
); );

View File

@ -60,10 +60,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
private authService: AuthService, private authService: AuthService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
private domainSettingsService: DomainSettingsService, private domainSettingsService: DomainSettingsService,
private taskSchedulerService: TaskSchedulerService, private taskSchedulerService?: TaskSchedulerService,
private logService?: LogService, private logService?: LogService,
) { ) {
void this.taskSchedulerService.registerTaskHandler( void this.taskSchedulerService?.registerTaskHandler(
ScheduledTaskNames.fido2ClientAbortTimeout, ScheduledTaskNames.fido2ClientAbortTimeout,
() => this.timeoutAbortController?.abort(), () => this.timeoutAbortController?.abort(),
); );
@ -229,7 +229,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
}; };
} }
await this.taskSchedulerService.clearScheduledTask({ await this.taskSchedulerService?.clearScheduledTask({
taskName: ScheduledTaskNames.fido2ClientAbortTimeout, taskName: ScheduledTaskNames.fido2ClientAbortTimeout,
timeoutId: timeout, timeoutId: timeout,
}); });
@ -334,7 +334,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
this.logService?.info(`[Fido2Client] Aborted with AbortController`); this.logService?.info(`[Fido2Client] Aborted with AbortController`);
throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
} }
await this.taskSchedulerService.clearScheduledTask({ await this.taskSchedulerService?.clearScheduledTask({
taskName: ScheduledTaskNames.fido2ClientAbortTimeout, taskName: ScheduledTaskNames.fido2ClientAbortTimeout,
timeoutId: timeout, timeoutId: timeout,
}); });
@ -368,7 +368,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
} }
this.timeoutAbortController = abortController; this.timeoutAbortController = abortController;
return await this.taskSchedulerService.setTimeout( return await this.taskSchedulerService?.setTimeout(
ScheduledTaskNames.fido2ClientAbortTimeout, ScheduledTaskNames.fido2ClientAbortTimeout,
clampedTimeout, clampedTimeout,
); );