mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[EC-272] Web workers using EncryptionService (#3532)
* Add item decryption to encryptService * Create multithreadEncryptService subclass to handle web workers * Create encryption web worker * Refactor cipherService to use new interface * Update dependencies
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { IEncrypted } from "../interfaces/IEncrypted";
|
||||
import { Decryptable } from "../interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../interfaces/initializer-metadata.interface";
|
||||
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class AbstractEncryptService {
|
||||
export abstract class EncryptService {
|
||||
abstract encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract encryptToBytes: (
|
||||
plainValue: ArrayBuffer,
|
||||
@@ -12,4 +14,8 @@ export abstract class AbstractEncryptService {
|
||||
abstract decryptToUtf8: (encString: EncString, key: SymmetricCryptoKey) => Promise<string>;
|
||||
abstract decryptToBytes: (encThing: IEncrypted, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
abstract resolveLegacyKey: (key: SymmetricCryptoKey, encThing: IEncrypted) => SymmetricCryptoKey;
|
||||
abstract decryptItems: <T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey
|
||||
) => Promise<T[]>;
|
||||
}
|
||||
12
libs/common/src/interfaces/decryptable.interface.ts
Normal file
12
libs/common/src/interfaces/decryptable.interface.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { InitializerMetadata } from "./initializer-metadata.interface";
|
||||
|
||||
/**
|
||||
* An object that contains EncStrings and knows how to decrypt them. This is usually a domain object with the
|
||||
* corresponding view object as the type argument.
|
||||
* @example Cipher implements Decryptable<CipherView>
|
||||
*/
|
||||
export interface Decryptable<TDecrypted extends InitializerMetadata> extends InitializerMetadata {
|
||||
decrypt: (key?: SymmetricCryptoKey) => Promise<TDecrypted>;
|
||||
}
|
||||
11
libs/common/src/interfaces/initializer-metadata.interface.ts
Normal file
11
libs/common/src/interfaces/initializer-metadata.interface.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { InitializerKey } from "../services/cryptography/initializer-key";
|
||||
|
||||
/**
|
||||
* This interface enables deserialization of arbitrary objects by recording their class name as an enum, which
|
||||
* will survive serialization. The enum can then be matched to a constructor or factory method for deserialization.
|
||||
* See get-class-initializer.ts for the initializer map.
|
||||
*/
|
||||
export interface InitializerMetadata {
|
||||
initializerKey: InitializerKey;
|
||||
toJSON?: () => { initializerKey: InitializerKey };
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// required to avoid linting errors when there are no flags
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
export type SharedFlags = {};
|
||||
export type SharedFlags = {
|
||||
multithreadDecryption: boolean;
|
||||
};
|
||||
|
||||
// required to avoid linting errors when there are no flags
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
import { getHostname, parse } from "tldts";
|
||||
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
|
||||
const nodeURL = typeof window === "undefined" ? require("url") : null;
|
||||
@@ -14,7 +14,7 @@ declare global {
|
||||
|
||||
interface BitwardenContainerService {
|
||||
getCryptoService: () => CryptoService;
|
||||
getEncryptService: () => AbstractEncryptService;
|
||||
getEncryptService: () => EncryptService;
|
||||
}
|
||||
|
||||
export class Utils {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Jsonify } from "type-fest";
|
||||
|
||||
import { CipherRepromptType } from "../../enums/cipherRepromptType";
|
||||
import { CipherType } from "../../enums/cipherType";
|
||||
import { Decryptable } from "../../interfaces/decryptable.interface";
|
||||
import { InitializerKey } from "../../services/cryptography/initializer-key";
|
||||
import { CipherData } from "../data/cipher.data";
|
||||
import { LocalData } from "../data/local.data";
|
||||
import { CipherView } from "../view/cipher.view";
|
||||
@@ -17,7 +19,9 @@ import { Password } from "./password";
|
||||
import { SecureNote } from "./secure-note";
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class Cipher extends Domain {
|
||||
export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
readonly initializerKey = InitializerKey.Cipher;
|
||||
|
||||
id: string;
|
||||
organizationId: string;
|
||||
folderId: string;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Jsonify } from "type-fest";
|
||||
import { CipherRepromptType } from "../../enums/cipherRepromptType";
|
||||
import { CipherType } from "../../enums/cipherType";
|
||||
import { LinkedIdType } from "../../enums/linkedIdType";
|
||||
import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface";
|
||||
import { InitializerKey } from "../../services/cryptography/initializer-key";
|
||||
import { LocalData } from "../data/local.data";
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
@@ -15,7 +17,9 @@ import { PasswordHistoryView } from "./password-history.view";
|
||||
import { SecureNoteView } from "./secure-note.view";
|
||||
import { View } from "./view";
|
||||
|
||||
export class CipherView implements View {
|
||||
export class CipherView implements View, InitializerMetadata {
|
||||
readonly initializerKey = InitializerKey.CipherView;
|
||||
|
||||
id: string = null;
|
||||
organizationId: string = null;
|
||||
folderId: string = null;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { FileUploadService } from "../abstractions/fileUpload.service";
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
@@ -65,7 +66,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private i18nService: I18nService,
|
||||
private searchService: () => SearchService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService
|
||||
private stateService: StateService,
|
||||
private encryptService: EncryptService
|
||||
) {}
|
||||
|
||||
async getDecryptedCipherCache(): Promise<CipherView[]> {
|
||||
@@ -329,35 +331,50 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
@sequentialize(() => "getAllDecrypted")
|
||||
async getAllDecrypted(): Promise<CipherView[]> {
|
||||
const userId = await this.stateService.getUserId();
|
||||
if ((await this.getDecryptedCipherCache()) != null) {
|
||||
if (
|
||||
this.searchService != null &&
|
||||
(this.searchService().indexedEntityId ?? userId) !== userId
|
||||
) {
|
||||
await this.searchService().indexCiphers(userId, await this.getDecryptedCipherCache());
|
||||
}
|
||||
await this.reindexCiphers();
|
||||
return await this.getDecryptedCipherCache();
|
||||
}
|
||||
|
||||
const decCiphers: CipherView[] = [];
|
||||
const hasKey = await this.cryptoService.hasKey();
|
||||
if (!hasKey) {
|
||||
throw new Error("No key.");
|
||||
}
|
||||
|
||||
const promises: Promise<number>[] = [];
|
||||
const ciphers = await this.getAll();
|
||||
ciphers.forEach(async (cipher) => {
|
||||
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));
|
||||
});
|
||||
const orgKeys = await this.cryptoService.getOrgKeys();
|
||||
const userKey = await this.cryptoService.getKeyForUserEncryption();
|
||||
|
||||
// Group ciphers by orgId or under 'null' for the user's ciphers
|
||||
const grouped = ciphers.reduce((agg, c) => {
|
||||
agg[c.organizationId] ??= [];
|
||||
agg[c.organizationId].push(c);
|
||||
return agg;
|
||||
}, {} as Record<string, Cipher[]>);
|
||||
|
||||
const decCiphers = (
|
||||
await Promise.all(
|
||||
Object.entries(grouped).map(([orgId, groupedCiphers]) =>
|
||||
this.encryptService.decryptItems(groupedCiphers, orgKeys.get(orgId) ?? userKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.sort(this.getLocaleSortingFunction());
|
||||
|
||||
await Promise.all(promises);
|
||||
decCiphers.sort(this.getLocaleSortingFunction());
|
||||
await this.setDecryptedCipherCache(decCiphers);
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
private async reindexCiphers() {
|
||||
const userId = await this.stateService.getUserId();
|
||||
const reindexRequired =
|
||||
this.searchService != null && (this.searchService().indexedEntityId ?? userId) !== userId;
|
||||
if (reindexRequired) {
|
||||
await this.searchService().indexCiphers(userId, await this.getDecryptedCipherCache());
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDecryptedForGrouping(groupingId: string, folder = true): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted();
|
||||
|
||||
@@ -488,21 +505,17 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
|
||||
const ciphers = await this.apiService.getCiphersOrganization(organizationId);
|
||||
if (ciphers != null && ciphers.data != null && ciphers.data.length) {
|
||||
const decCiphers: CipherView[] = [];
|
||||
const promises: any[] = [];
|
||||
ciphers.data.forEach((r) => {
|
||||
const data = new CipherData(r);
|
||||
const cipher = new Cipher(data);
|
||||
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
decCiphers.sort(this.getLocaleSortingFunction());
|
||||
return decCiphers;
|
||||
} else {
|
||||
const response = await this.apiService.getCiphersOrganization(organizationId);
|
||||
if (response?.data == null || response.data.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr)));
|
||||
const key = await this.cryptoService.getOrgKey(organizationId);
|
||||
const decCiphers = await this.encryptService.decryptItems(ciphers, key);
|
||||
|
||||
decCiphers.sort(this.getLocaleSortingFunction());
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
async getLastUsedForUrl(url: string, autofillOnPageLoad = false): Promise<CipherView> {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
export class ContainerService {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private encryptService: AbstractEncryptService
|
||||
) {}
|
||||
constructor(private cryptoService: CryptoService, private encryptService: EncryptService) {}
|
||||
|
||||
attachToGlobal(global: any) {
|
||||
if (!global.bitwardenContainerService) {
|
||||
@@ -26,7 +23,7 @@ export class ContainerService {
|
||||
/**
|
||||
* @throws Will throw if EncryptService was not instantiated and provided to the ContainerService constructor
|
||||
*/
|
||||
getEncryptService(): AbstractEncryptService {
|
||||
getEncryptService(): EncryptService {
|
||||
if (this.encryptService == null) {
|
||||
throw new Error("ContainerService.encryptService not initialized.");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as bigInt from "big-integer";
|
||||
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
@@ -25,7 +25,7 @@ import { ProfileProviderResponse } from "../models/response/profile-provider.res
|
||||
export class CryptoService implements CryptoServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private encryptService: AbstractEncryptService,
|
||||
private encryptService: EncryptService,
|
||||
protected platformUtilService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected stateService: StateService
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { EncryptionType } from "../enums/encryptionType";
|
||||
import { IEncrypted } from "../interfaces/IEncrypted";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { EncryptedObject } from "../models/domain/encrypted-object";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CryptoFunctionService } from "../../abstractions/cryptoFunction.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { EncryptionType } from "../../enums/encryptionType";
|
||||
import { IEncrypted } from "../../interfaces/IEncrypted";
|
||||
import { Decryptable } from "../../interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { EncArrayBuffer } from "../../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../models/domain/enc-string";
|
||||
import { EncryptedObject } from "../../models/domain/encrypted-object";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
|
||||
export class EncryptService implements AbstractEncryptService {
|
||||
export class EncryptServiceImplementation implements EncryptService {
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private logService: LogService,
|
||||
private logMacFailures: boolean
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected logService: LogService,
|
||||
protected logMacFailures: boolean
|
||||
) {}
|
||||
|
||||
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
@@ -148,6 +150,17 @@ export class EncryptService implements AbstractEncryptService {
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<T[]> {
|
||||
if (items == null || items.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await Promise.all(items.map((item) => item.decrypt(key)));
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = key;
|
||||
56
libs/common/src/services/cryptography/encrypt.worker.ts
Normal file
56
libs/common/src/services/cryptography/encrypt.worker.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Decryptable } from "../../interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "../../services/consoleLog.service";
|
||||
import { ContainerService } from "../../services/container.service";
|
||||
import { WebCryptoFunctionService } from "../../services/webCryptoFunction.service";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
import { getClassInitializer } from "./get-class-initializer";
|
||||
|
||||
const workerApi: Worker = self as any;
|
||||
|
||||
let inited = false;
|
||||
let encryptService: EncryptServiceImplementation;
|
||||
|
||||
/**
|
||||
* Bootstrap the worker environment with services required for decryption
|
||||
*/
|
||||
export function init() {
|
||||
const cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
const logService = new ConsoleLogService(false);
|
||||
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||
|
||||
const bitwardenContainerService = new ContainerService(null, encryptService);
|
||||
bitwardenContainerService.attachToGlobal(self);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for messages and decrypt their contents
|
||||
*/
|
||||
workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
if (!inited) {
|
||||
init();
|
||||
}
|
||||
|
||||
const request: {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
} = JSON.parse(event.data);
|
||||
|
||||
const key = SymmetricCryptoKey.fromJSON(request.key);
|
||||
const items = request.items.map((jsonItem) => {
|
||||
const initializer = getClassInitializer<Decryptable<any>>(jsonItem.initializerKey);
|
||||
return initializer(jsonItem);
|
||||
});
|
||||
const result = await encryptService.decryptItems(items, key);
|
||||
|
||||
workerApi.postMessage({
|
||||
id: request.id,
|
||||
items: JSON.stringify(result),
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface";
|
||||
import { Cipher } from "../../models/domain/cipher";
|
||||
import { CipherView } from "../../models/view/cipher.view";
|
||||
|
||||
import { InitializerKey } from "./initializer-key";
|
||||
|
||||
/**
|
||||
* Internal reference of classes so we can reconstruct objects properly.
|
||||
* Each entry should be keyed using the Decryptable.initializerKey property
|
||||
*/
|
||||
const classInitializers: Record<InitializerKey, (obj: any) => any> = {
|
||||
[InitializerKey.Cipher]: Cipher.fromJSON,
|
||||
[InitializerKey.CipherView]: CipherView.fromJSON,
|
||||
};
|
||||
|
||||
export function getClassInitializer<T extends InitializerMetadata>(
|
||||
className: InitializerKey
|
||||
): (obj: Jsonify<T>) => T {
|
||||
return classInitializers[className];
|
||||
}
|
||||
4
libs/common/src/services/cryptography/initializer-key.ts
Normal file
4
libs/common/src/services/cryptography/initializer-key.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum InitializerKey {
|
||||
Cipher = 0,
|
||||
CipherView = 1,
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { defaultIfEmpty, filter, firstValueFrom, fromEvent, map, Subject, takeUntil } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Decryptable } from "../../interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
import { getClassInitializer } from "./get-class-initializer";
|
||||
|
||||
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||
const workerTTL = 3 * 60000; // 3 minutes
|
||||
|
||||
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
||||
private worker: Worker;
|
||||
private timeout: any;
|
||||
|
||||
private clear$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Sends items to a web worker to decrypt them.
|
||||
* This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI).
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<T[]> {
|
||||
if (items == null || items.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.logService.info("Starting decryption using multithreading");
|
||||
|
||||
this.worker ??= new Worker(
|
||||
new URL("@bitwarden/common/services/cryptography/encrypt.worker.ts", import.meta.url)
|
||||
);
|
||||
|
||||
this.restartTimeout();
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
items: items,
|
||||
key: key,
|
||||
};
|
||||
|
||||
this.worker.postMessage(JSON.stringify(request));
|
||||
|
||||
return await firstValueFrom(
|
||||
fromEvent(this.worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
const initializer = getClassInitializer<T>(jsonItem.initializerKey);
|
||||
return initializer(jsonItem);
|
||||
})
|
||||
),
|
||||
takeUntil(this.clear$),
|
||||
defaultIfEmpty([])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.clear$.next();
|
||||
this.worker?.terminate();
|
||||
this.worker = null;
|
||||
this.clearTimeout();
|
||||
}
|
||||
|
||||
private restartTimeout() {
|
||||
this.clearTimeout();
|
||||
this.timeout = setTimeout(() => this.clear(), workerTTL);
|
||||
}
|
||||
|
||||
private clearTimeout() {
|
||||
if (this.timeout != null) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user