[PM-6426] Reworking implementation to leverage subscription based deregistration of alarms
This commit is contained in:
parent
ac67645000
commit
eb6ac491ec
|
@ -1,5 +1,5 @@
|
|||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Subscription } from "rxjs";
|
||||
|
||||
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
|
||||
|
@ -11,9 +11,13 @@ import { BrowserTaskSchedulerService } from "../../platform/services/abstraction
|
|||
import { ClearClipboard } from "./clear-clipboard";
|
||||
import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command";
|
||||
|
||||
jest.mock("rxjs", () => ({
|
||||
firstValueFrom: jest.fn(),
|
||||
}));
|
||||
jest.mock("rxjs", () => {
|
||||
const actual = jest.requireActual("rxjs");
|
||||
return {
|
||||
...actual,
|
||||
firstValueFrom: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("GeneratePasswordToClipboardCommand", () => {
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
|
@ -26,13 +30,15 @@ describe("GeneratePasswordToClipboardCommand", () => {
|
|||
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
autofillSettingsService = mock<AutofillSettingsService>();
|
||||
browserTaskSchedulerService = mock<BrowserTaskSchedulerService>({
|
||||
setTimeout: jest.fn(async (taskName, timeoutInMs) =>
|
||||
setTimeout(() => {
|
||||
setTimeout: jest.fn((taskName, timeoutInMs) => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (taskName === ScheduledTaskNames.generatePasswordClearClipboardTimeout) {
|
||||
void ClearClipboard.run();
|
||||
}
|
||||
}, timeoutInMs),
|
||||
),
|
||||
}, timeoutInMs);
|
||||
|
||||
return new Subscription(() => clearTimeout(timeoutHandle));
|
||||
}),
|
||||
});
|
||||
|
||||
passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Subscription } from "rxjs";
|
||||
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
|
||||
|
@ -10,7 +10,7 @@ import { ClearClipboard } from "./clear-clipboard";
|
|||
import { copyToClipboard } from "./copy-to-clipboard-command";
|
||||
|
||||
export class GeneratePasswordToClipboardCommand {
|
||||
private clearClipboardTimeout: number | NodeJS.Timeout;
|
||||
private clearClipboardSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
|
@ -39,11 +39,8 @@ export class GeneratePasswordToClipboardCommand {
|
|||
}
|
||||
|
||||
const timeoutInMs = clearClipboardDelayInSeconds * 1000;
|
||||
await this.taskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.generatePasswordClearClipboardTimeout,
|
||||
timeoutId: this.clearClipboardTimeout,
|
||||
});
|
||||
await this.taskSchedulerService.setTimeout(
|
||||
this.clearClipboardSubscription?.unsubscribe();
|
||||
this.clearClipboardSubscription = this.taskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.generatePasswordClearClipboardTimeout,
|
||||
timeoutInMs,
|
||||
);
|
||||
|
|
|
@ -101,21 +101,22 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
|
||||
describe("setTimeout", () => {
|
||||
it("triggers an error when setting a timeout for a task that is not registered", async () => {
|
||||
await expect(
|
||||
expect(() =>
|
||||
browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.notificationsReconnectTimeout,
|
||||
1000,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
).toThrow(
|
||||
`Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`,
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a timeout alarm", async () => {
|
||||
await browserTaskSchedulerService.setTimeout(
|
||||
browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMinutes * 60 * 1000,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(chrome.alarms.create).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
|
@ -128,7 +129,7 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
const mockAlarm = mock<chrome.alarms.Alarm>();
|
||||
chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(mockAlarm));
|
||||
|
||||
await browserTaskSchedulerService.setTimeout(
|
||||
browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMinutes * 60 * 1000,
|
||||
);
|
||||
|
@ -136,26 +137,28 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
expect(chrome.alarms.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when the task is scheduled to be triggered in less than 1 minute", () => {
|
||||
const delayInMs = 45000;
|
||||
describe("when the task is scheduled to be triggered in less than the minimum possible delay", () => {
|
||||
const delayInMs = 25000;
|
||||
|
||||
it("sets a timeout using the global setTimeout API", async () => {
|
||||
jest.spyOn(globalThis, "setTimeout");
|
||||
|
||||
await browserTaskSchedulerService.setTimeout(
|
||||
browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMs,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), delayInMs);
|
||||
});
|
||||
|
||||
it("sets a fallback alarm", async () => {
|
||||
const delayInMs = 15000;
|
||||
await browserTaskSchedulerService.setTimeout(
|
||||
browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMs,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(chrome.alarms.create).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
|
@ -167,10 +170,11 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
it("sets the fallback for a minimum of 1 minute if the environment not for Chrome", async () => {
|
||||
setupGlobalBrowserMock();
|
||||
|
||||
await browserTaskSchedulerService.setTimeout(
|
||||
browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMs,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(browser.alarms.create).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
|
@ -181,7 +185,7 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
it("clears the fallback alarm when the setTimeout is triggered", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
await browserTaskSchedulerService.setTimeout(
|
||||
browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMs,
|
||||
);
|
||||
|
@ -194,16 +198,47 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a subscription that can be used to clear the timeout", () => {
|
||||
jest.spyOn(globalThis, "clearTimeout");
|
||||
|
||||
const timeoutSubscription = browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
10000,
|
||||
);
|
||||
|
||||
timeoutSubscription.unsubscribe();
|
||||
|
||||
expect(chrome.alarms.clear).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(globalThis.clearTimeout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears alarms in non-chrome environments", () => {
|
||||
setupGlobalBrowserMock();
|
||||
|
||||
const timeoutSubscription = browserTaskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
10000,
|
||||
);
|
||||
timeoutSubscription.unsubscribe();
|
||||
|
||||
expect(browser.alarms.clear).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setInterval", () => {
|
||||
it("triggers an error when setting an interval for a task that is not registered", async () => {
|
||||
await expect(
|
||||
expect(() => {
|
||||
browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.notificationsReconnectTimeout,
|
||||
1000,
|
||||
),
|
||||
).rejects.toThrowError(
|
||||
);
|
||||
}).toThrow(
|
||||
`Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`,
|
||||
);
|
||||
});
|
||||
|
@ -212,10 +247,11 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
const intervalInMs = 10000;
|
||||
|
||||
it("sets up stepped alarms that trigger behavior after the first minute of setInterval execution", async () => {
|
||||
await browserTaskSchedulerService.setInterval(
|
||||
browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
intervalInMs,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(chrome.alarms.create).toHaveBeenCalledWith(
|
||||
`${ScheduledTaskNames.loginStrategySessionTimeout}__0`,
|
||||
|
@ -242,10 +278,11 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
it("sets an interval using the global setInterval API", async () => {
|
||||
jest.spyOn(globalThis, "setInterval");
|
||||
|
||||
await browserTaskSchedulerService.setInterval(
|
||||
browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
intervalInMs,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), intervalInMs);
|
||||
});
|
||||
|
@ -254,10 +291,11 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
jest.useFakeTimers();
|
||||
jest.spyOn(globalThis, "clearInterval");
|
||||
|
||||
await browserTaskSchedulerService.setInterval(
|
||||
browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
intervalInMs,
|
||||
);
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50000);
|
||||
|
||||
expect(globalThis.clearInterval).toHaveBeenCalledWith(expect.any(Number));
|
||||
|
@ -268,11 +306,12 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
const periodInMinutes = 2;
|
||||
const initialDelayInMs = 1000;
|
||||
|
||||
await browserTaskSchedulerService.setInterval(
|
||||
browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
periodInMinutes * 60 * 1000,
|
||||
initialDelayInMs,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(chrome.alarms.create).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
|
@ -283,10 +322,11 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
|
||||
it("defaults the alarm's delay in minutes to the interval in minutes if the delay is not specified", async () => {
|
||||
const periodInMinutes = 2;
|
||||
await browserTaskSchedulerService.setInterval(
|
||||
browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
periodInMinutes * 60 * 1000,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(chrome.alarms.create).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
|
@ -294,6 +334,52 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a subscription that can be used to clear an interval alarm", () => {
|
||||
jest.spyOn(globalThis, "clearInterval");
|
||||
|
||||
const intervalSubscription = browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
600000,
|
||||
);
|
||||
|
||||
intervalSubscription.unsubscribe();
|
||||
|
||||
expect(chrome.alarms.clear).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(globalThis.clearInterval).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a subscription that can be used to clear all stepped interval alarms", () => {
|
||||
jest.spyOn(globalThis, "clearInterval");
|
||||
|
||||
const intervalSubscription = browserTaskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
10000,
|
||||
);
|
||||
|
||||
intervalSubscription.unsubscribe();
|
||||
|
||||
expect(chrome.alarms.clear).toHaveBeenCalledWith(
|
||||
`${ScheduledTaskNames.loginStrategySessionTimeout}__0`,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(chrome.alarms.clear).toHaveBeenCalledWith(
|
||||
`${ScheduledTaskNames.loginStrategySessionTimeout}__1`,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(chrome.alarms.clear).toHaveBeenCalledWith(
|
||||
`${ScheduledTaskNames.loginStrategySessionTimeout}__2`,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(chrome.alarms.clear).toHaveBeenCalledWith(
|
||||
`${ScheduledTaskNames.loginStrategySessionTimeout}__3`,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(globalThis.clearInterval).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyAlarmsState", () => {
|
||||
|
@ -351,37 +437,6 @@ describe("BrowserTaskSchedulerService", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("clearScheduledTask", () => {
|
||||
it("skips clearing an alarm if the task name is not passed", async () => {
|
||||
await browserTaskSchedulerService.clearScheduledTask({ timeoutId: 1 });
|
||||
|
||||
expect(chrome.alarms.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears the alarm associated with the task", async () => {
|
||||
await browserTaskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
});
|
||||
|
||||
expect(chrome.alarms.clear).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears the alarm associated with the task when in a non-Chrome environment", async () => {
|
||||
setupGlobalBrowserMock();
|
||||
|
||||
await browserTaskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
});
|
||||
|
||||
expect(browser.alarms.clear).toHaveBeenCalledWith(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearAllScheduledTasks", () => {
|
||||
it("clears all scheduled tasks and extension alarms", async () => {
|
||||
// @ts-expect-error mocking global state update method
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
import { firstValueFrom, map, Observable, Subscription } 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 { DefaultTaskSchedulerService } from "@bitwarden/common/platform/services/default-task-scheduler.service";
|
||||
import {
|
||||
|
@ -51,25 +50,30 @@ export class BrowserTaskSchedulerServiceImplementation
|
|||
* @param taskName - The name of the task, used in defining the alarm.
|
||||
* @param delayInMs - The delay in milliseconds.
|
||||
*/
|
||||
async setTimeout(
|
||||
taskName: ScheduledTaskName,
|
||||
delayInMs: number,
|
||||
): Promise<number | NodeJS.Timeout> {
|
||||
setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription {
|
||||
let timeoutHandle: number | NodeJS.Timeout;
|
||||
this.validateRegisteredTask(taskName);
|
||||
|
||||
const delayInMinutes = delayInMs / 1000 / 60;
|
||||
await this.scheduleAlarm(taskName, {
|
||||
void this.scheduleAlarm(taskName, {
|
||||
delayInMinutes: this.getUpperBoundDelayInMinutes(delayInMinutes),
|
||||
});
|
||||
|
||||
// If the delay is less than a minute, we want to attempt to trigger the task through a setTimeout.
|
||||
// The alarm previously scheduled will be used as a backup in case the setTimeout fails.
|
||||
if (delayInMinutes < this.getUpperBoundDelayInMinutes(delayInMinutes)) {
|
||||
return globalThis.setTimeout(async () => {
|
||||
timeoutHandle = globalThis.setTimeout(async () => {
|
||||
await this.clearScheduledAlarm(taskName);
|
||||
await this.triggerTask(taskName);
|
||||
}, delayInMs);
|
||||
}
|
||||
|
||||
return new Subscription(() => {
|
||||
if (timeoutHandle) {
|
||||
globalThis.clearTimeout(timeoutHandle);
|
||||
}
|
||||
void this.clearScheduledAlarm(taskName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,11 +85,11 @@ export class BrowserTaskSchedulerServiceImplementation
|
|||
* @param intervalInMs - The interval in milliseconds.
|
||||
* @param initialDelayInMs - The initial delay in milliseconds.
|
||||
*/
|
||||
async setInterval(
|
||||
setInterval(
|
||||
taskName: ScheduledTaskName,
|
||||
intervalInMs: number,
|
||||
initialDelayInMs?: number,
|
||||
): Promise<number | NodeJS.Timeout> {
|
||||
): Subscription {
|
||||
this.validateRegisteredTask(taskName);
|
||||
|
||||
const intervalInMinutes = intervalInMs / 1000 / 60;
|
||||
|
@ -97,10 +101,12 @@ export class BrowserTaskSchedulerServiceImplementation
|
|||
return this.setupSteppedIntervalAlarms(taskName, intervalInMs);
|
||||
}
|
||||
|
||||
await this.scheduleAlarm(taskName, {
|
||||
void this.scheduleAlarm(taskName, {
|
||||
periodInMinutes: this.getUpperBoundDelayInMinutes(intervalInMinutes),
|
||||
delayInMinutes: this.getUpperBoundDelayInMinutes(initialDelayInMinutes),
|
||||
});
|
||||
|
||||
return new Subscription(() => this.clearScheduledAlarm(taskName));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,61 +118,50 @@ export class BrowserTaskSchedulerServiceImplementation
|
|||
* @param taskName - The name of the task
|
||||
* @param intervalInMs - The interval in milliseconds.
|
||||
*/
|
||||
private async setupSteppedIntervalAlarms(
|
||||
private setupSteppedIntervalAlarms(
|
||||
taskName: ScheduledTaskName,
|
||||
intervalInMs: number,
|
||||
): Promise<number | NodeJS.Timeout> {
|
||||
): Subscription {
|
||||
const alarmMinDelayInMinutes = this.getAlarmMinDelayInMinutes();
|
||||
const intervalInMinutes = intervalInMs / 1000 / 60;
|
||||
const numberOfAlarmsToCreate = Math.ceil(Math.ceil(1 / intervalInMinutes) / 2) + 1;
|
||||
const steppedAlarmPeriodInMinutes = alarmMinDelayInMinutes + intervalInMinutes;
|
||||
const steppedAlarmNames: string[] = [];
|
||||
for (let alarmIndex = 0; alarmIndex < numberOfAlarmsToCreate; alarmIndex++) {
|
||||
const steppedAlarmName = `${taskName}__${alarmIndex}`;
|
||||
steppedAlarmNames.push(steppedAlarmName);
|
||||
|
||||
const delayInMinutes = this.getUpperBoundDelayInMinutes(
|
||||
alarmMinDelayInMinutes + intervalInMinutes * alarmIndex,
|
||||
);
|
||||
|
||||
await this.clearScheduledAlarm(steppedAlarmName);
|
||||
|
||||
await this.scheduleAlarm(steppedAlarmName, {
|
||||
periodInMinutes: steppedAlarmPeriodInMinutes,
|
||||
delayInMinutes,
|
||||
void this.clearScheduledAlarm(steppedAlarmName).then(() => {
|
||||
void this.scheduleAlarm(steppedAlarmName, {
|
||||
periodInMinutes: steppedAlarmPeriodInMinutes,
|
||||
delayInMinutes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let elapsedMs = 0;
|
||||
const intervalId: number | NodeJS.Timeout = globalThis.setInterval(async () => {
|
||||
const intervalHandle: number | NodeJS.Timeout = globalThis.setInterval(async () => {
|
||||
elapsedMs += intervalInMs;
|
||||
const elapsedMinutes = elapsedMs / 1000 / 60;
|
||||
|
||||
if (elapsedMinutes >= alarmMinDelayInMinutes) {
|
||||
globalThis.clearInterval(intervalId);
|
||||
globalThis.clearInterval(intervalHandle);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.triggerTask(taskName, intervalInMinutes);
|
||||
}, intervalInMs);
|
||||
|
||||
return intervalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a scheduled task by its task identifier. If the task identifier
|
||||
* contains a task name, it will clear the browser extension alarm with that
|
||||
* name.
|
||||
*
|
||||
* @param taskIdentifier - The task identifier containing the task name.
|
||||
*/
|
||||
async clearScheduledTask(taskIdentifier: TaskIdentifier): Promise<void> {
|
||||
void super.clearScheduledTask(taskIdentifier);
|
||||
|
||||
const { taskName } = taskIdentifier;
|
||||
if (!taskName) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.clearScheduledAlarm(taskName);
|
||||
return new Subscription(() => {
|
||||
if (intervalHandle) {
|
||||
globalThis.clearInterval(intervalHandle);
|
||||
}
|
||||
steppedAlarmNames.forEach((alarmName) => void this.clearScheduledAlarm(alarmName));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
Subscription,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
@ -72,7 +73,7 @@ import {
|
|||
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
private sessionTimeout: number | NodeJS.Timeout;
|
||||
private sessionTimeoutSubscription: Subscription;
|
||||
private currentAuthnTypeState: GlobalState<AuthenticationType | null>;
|
||||
private loginStrategyCacheState: GlobalState<CacheData | null>;
|
||||
private loginStrategyCacheExpirationState: GlobalState<Date | null>;
|
||||
|
@ -319,7 +320,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
await this.loginStrategyCacheExpirationState.update(
|
||||
(_) => new Date(Date.now() + sessionTimeoutLength),
|
||||
);
|
||||
this.sessionTimeout = await this.taskSchedulerService.setTimeout(
|
||||
this.sessionTimeoutSubscription = this.taskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
sessionTimeoutLength,
|
||||
);
|
||||
|
@ -327,10 +328,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
|
||||
private async clearSessionTimeout(): Promise<void> {
|
||||
await this.loginStrategyCacheExpirationState.update((_) => null);
|
||||
await this.taskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
timeoutId: this.sessionTimeout,
|
||||
});
|
||||
this.sessionTimeoutSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private async isSessionValid(): Promise<boolean> {
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
import { ScheduledTaskName } from "../enums/scheduled-task-name.enum";
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
export type TaskIdentifier = {
|
||||
taskName?: ScheduledTaskName;
|
||||
timeoutId?: number | NodeJS.Timeout;
|
||||
intervalId?: number | NodeJS.Timeout;
|
||||
};
|
||||
import { ScheduledTaskName } from "../enums/scheduled-task-name.enum";
|
||||
|
||||
export abstract class TaskSchedulerService {
|
||||
protected taskHandlers: Map<string, () => void>;
|
||||
abstract setTimeout(
|
||||
taskName: ScheduledTaskName,
|
||||
delayInMs: number,
|
||||
): Promise<number | NodeJS.Timeout>;
|
||||
abstract setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription;
|
||||
abstract setInterval(
|
||||
taskName: ScheduledTaskName,
|
||||
intervalInMs: number,
|
||||
initialDelayInMs?: number,
|
||||
): Promise<number | NodeJS.Timeout>;
|
||||
abstract clearScheduledTask(taskIdentifier: TaskIdentifier): Promise<void>;
|
||||
): Subscription;
|
||||
abstract registerTaskHandler(taskName: ScheduledTaskName, handler: () => void): void;
|
||||
abstract unregisterTaskHandler(taskName: ScheduledTaskName): void;
|
||||
protected abstract triggerTask(taskName: ScheduledTaskName, periodInMinutes?: number): void;
|
||||
|
|
|
@ -28,17 +28,17 @@ describe("DefaultTaskSchedulerService", () => {
|
|||
});
|
||||
|
||||
it("triggers an error when setting a timeout for a task that is not registered", async () => {
|
||||
await expect(
|
||||
expect(() =>
|
||||
taskSchedulerService.setTimeout(ScheduledTaskNames.notificationsReconnectTimeout, 1000),
|
||||
).rejects.toThrowError(
|
||||
).toThrow(
|
||||
`Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`,
|
||||
);
|
||||
});
|
||||
|
||||
it("triggers an error when setting an interval for a task that is not registered", async () => {
|
||||
await expect(
|
||||
expect(() =>
|
||||
taskSchedulerService.setInterval(ScheduledTaskNames.notificationsReconnectTimeout, 1000),
|
||||
).rejects.toThrowError(
|
||||
).toThrow(
|
||||
`Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`,
|
||||
);
|
||||
});
|
||||
|
@ -57,8 +57,8 @@ describe("DefaultTaskSchedulerService", () => {
|
|||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("sets a timeout and returns the timeout id", async () => {
|
||||
const timeoutId = await taskSchedulerService.setTimeout(
|
||||
it("sets a timeout and returns the timeout id", () => {
|
||||
const timeoutId = taskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMs,
|
||||
);
|
||||
|
@ -71,8 +71,8 @@ describe("DefaultTaskSchedulerService", () => {
|
|||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets an interval timeout and results the interval id", async () => {
|
||||
const intervalId = await taskSchedulerService.setInterval(
|
||||
it("sets an interval timeout and results the interval id", () => {
|
||||
const intervalId = taskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
intervalInMs,
|
||||
);
|
||||
|
@ -89,32 +89,32 @@ describe("DefaultTaskSchedulerService", () => {
|
|||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("clears scheduled tasks using the timeout id", async () => {
|
||||
const timeoutId = await taskSchedulerService.setTimeout(
|
||||
it("clears scheduled tasks using the timeout id", () => {
|
||||
const timeoutHandle = taskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
delayInMs,
|
||||
);
|
||||
|
||||
expect(timeoutId).toBeDefined();
|
||||
expect(timeoutHandle).toBeDefined();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await taskSchedulerService.clearScheduledTask({ timeoutId });
|
||||
timeoutHandle.unsubscribe();
|
||||
|
||||
jest.advanceTimersByTime(delayInMs);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears scheduled tasks using the interval id", async () => {
|
||||
const intervalId = await taskSchedulerService.setInterval(
|
||||
it("clears scheduled tasks using the interval id", () => {
|
||||
const intervalHandle = taskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.loginStrategySessionTimeout,
|
||||
intervalInMs,
|
||||
);
|
||||
|
||||
expect(intervalId).toBeDefined();
|
||||
expect(intervalHandle).toBeDefined();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await taskSchedulerService.clearScheduledTask({ intervalId });
|
||||
intervalHandle.unsubscribe();
|
||||
|
||||
jest.advanceTimersByTime(intervalInMs);
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Subscription } from "rxjs";
|
||||
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { TaskIdentifier, TaskSchedulerService } from "../abstractions/task-scheduler.service";
|
||||
import { TaskSchedulerService } from "../abstractions/task-scheduler.service";
|
||||
import { ScheduledTaskName } from "../enums/scheduled-task-name.enum";
|
||||
|
||||
export class DefaultTaskSchedulerService extends TaskSchedulerService {
|
||||
|
@ -15,13 +17,11 @@ export class DefaultTaskSchedulerService extends TaskSchedulerService {
|
|||
* @param taskName - The name of the task. Unused in the base implementation.
|
||||
* @param delayInMs - The delay in milliseconds.
|
||||
*/
|
||||
async setTimeout(
|
||||
taskName: ScheduledTaskName,
|
||||
delayInMs: number,
|
||||
): Promise<number | NodeJS.Timeout> {
|
||||
setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription {
|
||||
this.validateRegisteredTask(taskName);
|
||||
|
||||
return globalThis.setTimeout(() => this.triggerTask(taskName), delayInMs);
|
||||
const timeoutHandle = globalThis.setTimeout(() => this.triggerTask(taskName), delayInMs);
|
||||
return new Subscription(() => globalThis.clearTimeout(timeoutHandle));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,29 +31,16 @@ export class DefaultTaskSchedulerService extends TaskSchedulerService {
|
|||
* @param intervalInMs - The interval in milliseconds.
|
||||
* @param _initialDelayInMs - The initial delay in milliseconds. Unused in the base implementation.
|
||||
*/
|
||||
async setInterval(
|
||||
setInterval(
|
||||
taskName: ScheduledTaskName,
|
||||
intervalInMs: number,
|
||||
_initialDelayInMs?: number,
|
||||
): Promise<number | NodeJS.Timeout> {
|
||||
): Subscription {
|
||||
this.validateRegisteredTask(taskName);
|
||||
|
||||
return globalThis.setInterval(() => this.triggerTask(taskName), intervalInMs);
|
||||
}
|
||||
const intervalHandle = globalThis.setInterval(() => this.triggerTask(taskName), intervalInMs);
|
||||
|
||||
/**
|
||||
* Clears a scheduled task.
|
||||
*
|
||||
* @param taskIdentifier - The task identifier containing the timeout or interval id.
|
||||
*/
|
||||
async clearScheduledTask(taskIdentifier: TaskIdentifier): Promise<void> {
|
||||
if (taskIdentifier.timeoutId) {
|
||||
globalThis.clearTimeout(taskIdentifier.timeoutId);
|
||||
}
|
||||
|
||||
if (taskIdentifier.intervalId) {
|
||||
globalThis.clearInterval(taskIdentifier.intervalId);
|
||||
}
|
||||
return new Subscription(() => globalThis.clearInterval(intervalHandle));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { firstValueFrom, map, timeout } from "rxjs";
|
||||
import { firstValueFrom, map, Subscription, timeout } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
|
@ -19,7 +19,7 @@ import { Utils } from "../misc/utils";
|
|||
|
||||
export class SystemService implements SystemServiceAbstraction {
|
||||
private reloadInterval: any = null;
|
||||
private clearClipboardTimeout: any = null;
|
||||
private clearClipboardTimeoutSubscription: Subscription;
|
||||
private clearClipboardTimeoutFunction: () => Promise<any> = null;
|
||||
|
||||
constructor(
|
||||
|
@ -121,10 +121,7 @@ export class SystemService implements SystemServiceAbstraction {
|
|||
}
|
||||
|
||||
async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> {
|
||||
await this.taskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.systemClearClipboardTimeout,
|
||||
timeoutId: this.clearClipboardTimeout,
|
||||
});
|
||||
this.clearClipboardTimeoutSubscription?.unsubscribe();
|
||||
|
||||
if (Utils.isNullOrWhitespace(clipboardValue)) {
|
||||
return;
|
||||
|
@ -149,7 +146,7 @@ export class SystemService implements SystemServiceAbstraction {
|
|||
}
|
||||
};
|
||||
|
||||
this.clearClipboardTimeout = this.taskSchedulerService.setTimeout(
|
||||
this.clearClipboardTimeoutSubscription = this.taskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.systemClearClipboardTimeout,
|
||||
taskTimeoutInMs,
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as signalR from "@microsoft/signalr";
|
||||
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Subscription } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../../../auth/src/common/abstractions";
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
|
@ -30,7 +30,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
|||
private connected = false;
|
||||
private inited = false;
|
||||
private inactive = false;
|
||||
private reconnectTimer: number | NodeJS.Timeout = null;
|
||||
private reconnectTimerSubscription: Subscription;
|
||||
private isSyncingOnReconnect = true;
|
||||
|
||||
constructor(
|
||||
|
@ -225,10 +225,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
|||
}
|
||||
|
||||
private async reconnect(sync: boolean) {
|
||||
await this.taskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.notificationsReconnectTimeout,
|
||||
timeoutId: this.reconnectTimer,
|
||||
});
|
||||
this.reconnectTimerSubscription?.unsubscribe();
|
||||
|
||||
if (this.connected || !this.inited || this.inactive) {
|
||||
return;
|
||||
|
@ -250,7 +247,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
|||
|
||||
if (!this.connected) {
|
||||
this.isSyncingOnReconnect = sync;
|
||||
this.reconnectTimer = await this.taskSchedulerService.setTimeout(
|
||||
this.reconnectTimerSubscription = this.taskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.notificationsReconnectTimeout,
|
||||
this.random(120000, 300000),
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Subscription } from "rxjs";
|
||||
import { parse } from "tldts";
|
||||
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
|
@ -179,7 +179,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||
this.logService?.info(`[Fido2Client] Aborted with AbortController`);
|
||||
throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
|
||||
}
|
||||
const timeout = await this.setAbortTimeout(
|
||||
const timeoutSubscription = this.setAbortTimeout(
|
||||
abortController,
|
||||
params.authenticatorSelection?.userVerification,
|
||||
params.timeout,
|
||||
|
@ -228,10 +228,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||
};
|
||||
}
|
||||
|
||||
await this.taskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.fido2ClientAbortTimeout,
|
||||
timeoutId: timeout,
|
||||
});
|
||||
timeoutSubscription?.unsubscribe();
|
||||
|
||||
return {
|
||||
credentialId: Fido2Utils.bufferToString(makeCredentialResult.credentialId),
|
||||
|
@ -292,7 +289,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||
throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
|
||||
}
|
||||
|
||||
const timeout = await this.setAbortTimeout(
|
||||
const timeoutSubscription = this.setAbortTimeout(
|
||||
abortController,
|
||||
params.userVerification,
|
||||
params.timeout,
|
||||
|
@ -333,10 +330,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||
this.logService?.info(`[Fido2Client] Aborted with AbortController`);
|
||||
throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
|
||||
}
|
||||
await this.taskSchedulerService.clearScheduledTask({
|
||||
taskName: ScheduledTaskNames.fido2ClientAbortTimeout,
|
||||
timeoutId: timeout,
|
||||
});
|
||||
|
||||
timeoutSubscription?.unsubscribe();
|
||||
|
||||
return {
|
||||
authenticatorData: Fido2Utils.bufferToString(getAssertionResult.authenticatorData),
|
||||
|
@ -350,11 +345,11 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||
};
|
||||
}
|
||||
|
||||
private setAbortTimeout = async (
|
||||
private setAbortTimeout = (
|
||||
abortController: AbortController,
|
||||
userVerification?: UserVerification,
|
||||
timeout?: number,
|
||||
): Promise<number | NodeJS.Timeout> => {
|
||||
): Subscription => {
|
||||
let clampedTimeout: number;
|
||||
|
||||
const { WITH_VERIFICATION, NO_VERIFICATION } = this.TIMEOUTS;
|
||||
|
@ -367,7 +362,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
|||
}
|
||||
|
||||
this.timeoutAbortController = abortController;
|
||||
return await this.taskSchedulerService.setTimeout(
|
||||
return this.taskSchedulerService.setTimeout(
|
||||
ScheduledTaskNames.fido2ClientAbortTimeout,
|
||||
clampedTimeout,
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue