1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[PM-12045] search service activeuserstate (#13035)

* removing activeuserstate from search service
This commit is contained in:
Jason Ng
2025-03-06 12:26:24 -05:00
committed by GitHub
parent 9761588a2a
commit f65daf7284
19 changed files with 159 additions and 637 deletions

View File

@@ -3,16 +3,21 @@
import { Observable } from "rxjs";
import { SendView } from "../tools/send/models/view/send.view";
import { IndexedEntityId } from "../types/guid";
import { IndexedEntityId, UserId } from "../types/guid";
import { CipherView } from "../vault/models/view/cipher.view";
export abstract class SearchService {
indexedEntityId$: Observable<IndexedEntityId | null>;
indexedEntityId$: (userId: UserId) => Observable<IndexedEntityId | null>;
clearIndex: () => Promise<void>;
isSearchable: (query: string) => Promise<boolean>;
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise<void>;
clearIndex: (userId: UserId) => Promise<void>;
isSearchable: (userId: UserId, query: string) => Promise<boolean>;
indexCiphers: (
userId: UserId,
ciphersToIndex: CipherView[],
indexedEntityGuid?: string,
) => Promise<void>;
searchCiphers: (
userId: UserId,
query: string,
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
ciphers?: CipherView[],

View File

@@ -138,10 +138,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
);
if (userId == null || userId === currentUserId) {
await this.searchService.clearIndex();
await this.collectionService.clearActiveUserCache();
}
await this.searchService.clearIndex(lockingUserId);
await this.folderService.clearDecryptedFolderState(lockingUserId);
await this.masterPasswordService.clearMasterKey(lockingUserId);

View File

@@ -9,13 +9,13 @@ import { UriMatchStrategy } from "../models/domain/domain-service";
import { I18nService } from "../platform/abstractions/i18n.service";
import { LogService } from "../platform/abstractions/log.service";
import {
ActiveUserState,
SingleUserState,
StateProvider,
UserKeyDefinition,
VAULT_SEARCH_MEMORY,
} from "../platform/state";
import { SendView } from "../tools/send/models/view/send.view";
import { IndexedEntityId } from "../types/guid";
import { IndexedEntityId, UserId } from "../types/guid";
import { FieldType } from "../vault/enums";
import { CipherType } from "../vault/enums/cipher-type";
import { CipherView } from "../vault/models/view/cipher.view";
@@ -70,24 +70,6 @@ export const LUNR_SEARCH_INDEXING = new UserKeyDefinition<boolean>(
export class SearchService implements SearchServiceAbstraction {
private static registeredPipeline = false;
private searchIndexState: ActiveUserState<SerializedLunrIndex> =
this.stateProvider.getActive(LUNR_SEARCH_INDEX);
private readonly index$: Observable<lunr.Index | null> = this.searchIndexState.state$.pipe(
map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)),
);
private searchIndexEntityIdState: ActiveUserState<IndexedEntityId> = this.stateProvider.getActive(
LUNR_SEARCH_INDEXED_ENTITY_ID,
);
readonly indexedEntityId$: Observable<IndexedEntityId | null> =
this.searchIndexEntityIdState.state$.pipe(map((id) => id));
private searchIsIndexingState: ActiveUserState<boolean> =
this.stateProvider.getActive(LUNR_SEARCH_INDEXING);
private readonly searchIsIndexing$: Observable<boolean> = this.searchIsIndexingState.state$.pipe(
map((indexing) => indexing ?? false),
);
private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"];
private readonly defaultSearchableMinLength: number = 2;
private searchableMinLength: number = this.defaultSearchableMinLength;
@@ -114,15 +96,41 @@ export class SearchService implements SearchServiceAbstraction {
}
}
async clearIndex(): Promise<void> {
await this.searchIndexEntityIdState.update(() => null);
await this.searchIndexState.update(() => null);
await this.searchIsIndexingState.update(() => null);
private searchIndexState(userId: UserId): SingleUserState<SerializedLunrIndex> {
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEX);
}
async isSearchable(query: string): Promise<boolean> {
private index$(userId: UserId): Observable<lunr.Index | null> {
return this.searchIndexState(userId).state$.pipe(
map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)),
);
}
private searchIndexEntityIdState(userId: UserId): SingleUserState<IndexedEntityId | null> {
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEXED_ENTITY_ID);
}
indexedEntityId$(userId: UserId): Observable<IndexedEntityId | null> {
return this.searchIndexEntityIdState(userId).state$.pipe(map((id) => id));
}
private searchIsIndexingState(userId: UserId): SingleUserState<boolean> {
return this.stateProvider.getUser(userId, LUNR_SEARCH_INDEXING);
}
private searchIsIndexing$(userId: UserId): Observable<boolean> {
return this.searchIsIndexingState(userId).state$.pipe(map((indexing) => indexing ?? false));
}
async clearIndex(userId: UserId): Promise<void> {
await this.searchIndexEntityIdState(userId).update(() => null);
await this.searchIndexState(userId).update(() => null);
await this.searchIsIndexingState(userId).update(() => null);
}
async isSearchable(userId: UserId, query: string): Promise<boolean> {
query = SearchService.normalizeSearchQuery(query);
const index = await this.getIndexForSearch();
const index = await this.getIndexForSearch(userId);
const notSearchable =
query == null ||
(index == null && query.length < this.searchableMinLength) ||
@@ -130,13 +138,17 @@ export class SearchService implements SearchServiceAbstraction {
return !notSearchable;
}
async indexCiphers(ciphers: CipherView[], indexedEntityId?: string): Promise<void> {
if (await this.getIsIndexing()) {
async indexCiphers(
userId: UserId,
ciphers: CipherView[],
indexedEntityId?: string,
): Promise<void> {
if (await this.getIsIndexing(userId)) {
return;
}
await this.setIsIndexing(true);
await this.setIndexedEntityIdForSearch(indexedEntityId as IndexedEntityId);
await this.setIsIndexing(userId, true);
await this.setIndexedEntityIdForSearch(userId, indexedEntityId as IndexedEntityId);
const builder = new lunr.Builder();
builder.pipeline.add(this.normalizeAccentsPipelineFunction);
builder.ref("id");
@@ -172,14 +184,15 @@ export class SearchService implements SearchServiceAbstraction {
ciphers.forEach((c) => builder.add(c));
const index = builder.build();
await this.setIndexForSearch(index.toJSON() as SerializedLunrIndex);
await this.setIndexForSearch(userId, index.toJSON() as SerializedLunrIndex);
await this.setIsIndexing(false);
await this.setIsIndexing(userId, false);
this.logService.info("Finished search indexing");
}
async searchCiphers(
userId: UserId,
query: string,
filter: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[] = null,
ciphers: CipherView[],
@@ -202,18 +215,18 @@ export class SearchService implements SearchServiceAbstraction {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
}
if (!(await this.isSearchable(query))) {
if (!(await this.isSearchable(userId, query))) {
return ciphers;
}
if (await this.getIsIndexing()) {
if (await this.getIsIndexing(userId)) {
await new Promise((r) => setTimeout(r, 250));
if (await this.getIsIndexing()) {
if (await this.getIsIndexing(userId)) {
await new Promise((r) => setTimeout(r, 500));
}
}
const index = await this.getIndexForSearch();
const index = await this.getIndexForSearch(userId);
if (index == null) {
// Fall back to basic search if index is not available
return this.searchCiphersBasic(ciphers, query);
@@ -307,24 +320,27 @@ export class SearchService implements SearchServiceAbstraction {
return sendsMatched.concat(lowPriorityMatched);
}
async getIndexForSearch(): Promise<lunr.Index | null> {
return await firstValueFrom(this.index$);
async getIndexForSearch(userId: UserId): Promise<lunr.Index | null> {
return await firstValueFrom(this.index$(userId));
}
private async setIndexForSearch(index: SerializedLunrIndex): Promise<void> {
await this.searchIndexState.update(() => index);
private async setIndexForSearch(userId: UserId, index: SerializedLunrIndex): Promise<void> {
await this.searchIndexState(userId).update(() => index);
}
private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise<void> {
await this.searchIndexEntityIdState.update(() => indexedEntityId);
private async setIndexedEntityIdForSearch(
userId: UserId,
indexedEntityId: IndexedEntityId,
): Promise<void> {
await this.searchIndexEntityIdState(userId).update(() => indexedEntityId);
}
private async setIsIndexing(indexing: boolean): Promise<void> {
await this.searchIsIndexingState.update(() => indexing);
private async setIsIndexing(userId: UserId, indexing: boolean): Promise<void> {
await this.searchIsIndexingState(userId).update(() => indexing);
}
private async getIsIndexing(): Promise<boolean> {
return await firstValueFrom(this.searchIsIndexing$);
private async getIsIndexing(userId: UserId): Promise<boolean> {
return await firstValueFrom(this.searchIsIndexing$(userId));
}
private fieldExtractor(c: CipherView, joined: boolean) {

View File

@@ -363,7 +363,8 @@ describe("Cipher Service", () => {
configService.getFeatureFlag.mockResolvedValue(true);
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
searchService.indexedEntityId$ = of(null);
searchService.indexedEntityId$.mockReturnValue(of(null));
stateService.getUserId.mockResolvedValue(mockUserId);
const keys = {

View File

@@ -165,9 +165,9 @@ export class CipherService implements CipherServiceAbstraction {
}
if (this.searchService != null) {
if (value == null) {
await this.searchService.clearIndex();
await this.searchService.clearIndex(userId);
} else {
await this.searchService.indexCiphers(value);
await this.searchService.indexCiphers(userId, value);
}
}
}
@@ -480,9 +480,9 @@ export class CipherService implements CipherServiceAbstraction {
private async reindexCiphers(userId: UserId) {
const reindexRequired =
this.searchService != null &&
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
((await firstValueFrom(this.searchService.indexedEntityId$(userId))) ?? userId) !== userId;
if (reindexRequired) {
await this.searchService.indexCiphers(await this.getDecryptedCiphers(userId), userId);
await this.searchService.indexCiphers(userId, await this.getDecryptedCiphers(userId), userId);
}
}