[PM-6426] Reworking implementation to leverage subscription based deregistration of alarms

This commit is contained in:
Cesar Gonzalez 2024-05-10 14:41:55 -05:00
parent ac67645000
commit eb6ac491ec
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
11 changed files with 207 additions and 188 deletions

View File

@ -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]);

View File

@ -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,
);

View File

@ -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

View File

@ -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));
});
}
/**

View File

@ -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> {

View File

@ -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;

View File

@ -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);

View File

@ -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));
}
/**

View File

@ -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,
);

View File

@ -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),
);

View File

@ -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,
);