mirror of
https://github.com/bitwarden/browser
synced 2025-12-28 22:23:28 +00:00
[PM-6194] Refactor injection of services in browser services module (#8380)
* refactored injector of services on the browser service module * refactored the search and popup serach service to use state provider * renamed back to default * removed token service that was readded during merge conflict * Updated search service construction on the cli * updated to use user key definition * Reafctored all components that refernce issearchable * removed commented variable * added uncommited code to remove dependencies not needed anymore * added uncommited code to remove dependencies not needed anymore
This commit is contained in:
@@ -726,7 +726,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SearchServiceAbstraction,
|
||||
useClass: SearchService,
|
||||
deps: [LogService, I18nServiceAbstraction],
|
||||
deps: [LogService, I18nServiceAbstraction, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NotificationsServiceAbstraction,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, firstValueFrom, mergeMap, takeUntil } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Subject,
|
||||
firstValueFrom,
|
||||
mergeMap,
|
||||
from,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -24,7 +32,6 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
expired = false;
|
||||
type: SendType = null;
|
||||
sends: SendView[] = [];
|
||||
searchText: string;
|
||||
selectedType: SendType;
|
||||
selectedAll: boolean;
|
||||
filter: (cipher: SendView) => boolean;
|
||||
@@ -39,6 +46,8 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
private searchTimeout: any;
|
||||
private destroy$ = new Subject<void>();
|
||||
private _filteredSends: SendView[];
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
protected isSearchable: boolean = false;
|
||||
|
||||
get filteredSends(): SendView[] {
|
||||
return this._filteredSends;
|
||||
@@ -48,6 +57,14 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
this._filteredSends = filteredSends;
|
||||
}
|
||||
|
||||
get searchText() {
|
||||
return this._searchText$.value;
|
||||
}
|
||||
|
||||
set searchText(value: string) {
|
||||
this._searchText$.next(value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected sendService: SendService,
|
||||
protected i18nService: I18nService,
|
||||
@@ -68,6 +85,15 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
.subscribe((policyAppliesToActiveUser) => {
|
||||
this.disableSend = policyAppliesToActiveUser;
|
||||
});
|
||||
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
this.isSearchable = isSearchable;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -122,14 +148,14 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
if (timeout == null) {
|
||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
||||
this.hasSearched = this.isSearchable;
|
||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||
this.applyTextSearch();
|
||||
return;
|
||||
}
|
||||
this.searchPending = true;
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
this.hasSearched = this.searchService.isSearchable(this.searchText);
|
||||
this.hasSearched = this.isSearchable;
|
||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||
this.applyTextSearch();
|
||||
this.searchPending = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -6,7 +7,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@Directive()
|
||||
export class VaultItemsComponent {
|
||||
export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
@Input() activeCipherId: string = null;
|
||||
@Output() onCipherClicked = new EventEmitter<CipherView>();
|
||||
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
|
||||
@@ -23,13 +24,15 @@ export class VaultItemsComponent {
|
||||
|
||||
protected searchPending = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private searchTimeout: any = null;
|
||||
private _searchText: string = null;
|
||||
private isSearchable: boolean = false;
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
get searchText() {
|
||||
return this._searchText;
|
||||
return this._searchText$.value;
|
||||
}
|
||||
set searchText(value: string) {
|
||||
this._searchText = value;
|
||||
this._searchText$.next(value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -37,6 +40,21 @@ export class VaultItemsComponent {
|
||||
protected cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
this.isSearchable = isSearchable;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||
this.deleted = deleted ?? false;
|
||||
await this.applyFilter(filter);
|
||||
@@ -90,7 +108,7 @@ export class VaultItemsComponent {
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return !this.searchPending && this.searchService.isSearchable(this.searchText);
|
||||
return !this.searchPending && this.isSearchable;
|
||||
}
|
||||
|
||||
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SendView } from "../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId } from "../types/guid";
|
||||
import { CipherView } from "../vault/models/view/cipher.view";
|
||||
|
||||
export abstract class SearchService {
|
||||
indexedEntityId?: string = null;
|
||||
clearIndex: () => void;
|
||||
isSearchable: (query: string) => boolean;
|
||||
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => void;
|
||||
indexedEntityId$: Observable<IndexedEntityId | null>;
|
||||
|
||||
clearIndex: () => Promise<void>;
|
||||
isSearchable: (query: string) => Promise<boolean>;
|
||||
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise<void>;
|
||||
searchCiphers: (
|
||||
query: string,
|
||||
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
|
||||
|
||||
@@ -127,3 +127,4 @@ export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk",
|
||||
web: "disk-local",
|
||||
});
|
||||
export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory");
|
||||
export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory");
|
||||
|
||||
@@ -1,20 +1,91 @@
|
||||
import * as lunr from "lunr";
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
|
||||
import { UriMatchStrategy } from "../models/domain/domain-service";
|
||||
import { I18nService } from "../platform/abstractions/i18n.service";
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import {
|
||||
ActiveUserState,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
VAULT_SEARCH_MEMORY,
|
||||
} from "../platform/state";
|
||||
import { SendView } from "../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId } from "../types/guid";
|
||||
import { FieldType } from "../vault/enums";
|
||||
import { CipherType } from "../vault/enums/cipher-type";
|
||||
import { CipherView } from "../vault/models/view/cipher.view";
|
||||
|
||||
export type SerializedLunrIndex = {
|
||||
version: string;
|
||||
fields: string[];
|
||||
fieldVectors: [string, number[]];
|
||||
invertedIndex: any[];
|
||||
pipeline: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The `KeyDefinition` for accessing the search index in application state.
|
||||
* The key definition is configured to clear the index when the user locks the vault.
|
||||
*/
|
||||
export const LUNR_SEARCH_INDEX = new UserKeyDefinition<SerializedLunrIndex>(
|
||||
VAULT_SEARCH_MEMORY,
|
||||
"searchIndex",
|
||||
{
|
||||
deserializer: (obj: Jsonify<SerializedLunrIndex>) => obj,
|
||||
clearOn: ["lock"],
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The `KeyDefinition` for accessing the ID of the entity currently indexed by Lunr search.
|
||||
* The key definition is configured to clear the indexed entity ID when the user locks the vault.
|
||||
*/
|
||||
export const LUNR_SEARCH_INDEXED_ENTITY_ID = new UserKeyDefinition<IndexedEntityId>(
|
||||
VAULT_SEARCH_MEMORY,
|
||||
"searchIndexedEntityId",
|
||||
{
|
||||
deserializer: (obj: Jsonify<IndexedEntityId>) => obj,
|
||||
clearOn: ["lock"],
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The `KeyDefinition` for accessing the state of Lunr search indexing, indicating whether the Lunr search index is currently being built or updating.
|
||||
* The key definition is configured to clear the indexing state when the user locks the vault.
|
||||
*/
|
||||
export const LUNR_SEARCH_INDEXING = new UserKeyDefinition<boolean>(
|
||||
VAULT_SEARCH_MEMORY,
|
||||
"isIndexing",
|
||||
{
|
||||
deserializer: (obj: Jsonify<boolean>) => obj,
|
||||
clearOn: ["lock"],
|
||||
},
|
||||
);
|
||||
|
||||
export class SearchService implements SearchServiceAbstraction {
|
||||
private static registeredPipeline = false;
|
||||
|
||||
indexedEntityId?: string = null;
|
||||
private indexing = false;
|
||||
private index: lunr.Index = null;
|
||||
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;
|
||||
@@ -22,6 +93,7 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.i18nService.locale$.subscribe((locale) => {
|
||||
if (this.immediateSearchLocales.indexOf(locale) !== -1) {
|
||||
@@ -40,28 +112,29 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
clearIndex(): void {
|
||||
this.indexedEntityId = null;
|
||||
this.index = null;
|
||||
async clearIndex(): Promise<void> {
|
||||
await this.searchIndexEntityIdState.update(() => null);
|
||||
await this.searchIndexState.update(() => null);
|
||||
await this.searchIsIndexingState.update(() => null);
|
||||
}
|
||||
|
||||
isSearchable(query: string): boolean {
|
||||
async isSearchable(query: string): Promise<boolean> {
|
||||
query = SearchService.normalizeSearchQuery(query);
|
||||
const index = await this.getIndexForSearch();
|
||||
const notSearchable =
|
||||
query == null ||
|
||||
(this.index == null && query.length < this.searchableMinLength) ||
|
||||
(this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
|
||||
(index == null && query.length < this.searchableMinLength) ||
|
||||
(index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
|
||||
return !notSearchable;
|
||||
}
|
||||
|
||||
indexCiphers(ciphers: CipherView[], indexedEntityId?: string): void {
|
||||
if (this.indexing) {
|
||||
async indexCiphers(ciphers: CipherView[], indexedEntityId?: string): Promise<void> {
|
||||
if (await this.getIsIndexing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.indexing = true;
|
||||
this.indexedEntityId = indexedEntityId;
|
||||
this.index = null;
|
||||
await this.setIsIndexing(true);
|
||||
await this.setIndexedEntityIdForSearch(indexedEntityId as IndexedEntityId);
|
||||
const builder = new lunr.Builder();
|
||||
builder.pipeline.add(this.normalizeAccentsPipelineFunction);
|
||||
builder.ref("id");
|
||||
@@ -95,9 +168,11 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
|
||||
ciphers = ciphers || [];
|
||||
ciphers.forEach((c) => builder.add(c));
|
||||
this.index = builder.build();
|
||||
const index = builder.build();
|
||||
|
||||
this.indexing = false;
|
||||
await this.setIndexForSearch(index.toJSON() as SerializedLunrIndex);
|
||||
|
||||
await this.setIsIndexing(false);
|
||||
|
||||
this.logService.info("Finished search indexing");
|
||||
}
|
||||
@@ -125,18 +200,18 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
|
||||
}
|
||||
|
||||
if (!this.isSearchable(query)) {
|
||||
if (!(await this.isSearchable(query))) {
|
||||
return ciphers;
|
||||
}
|
||||
|
||||
if (this.indexing) {
|
||||
if (await this.getIsIndexing()) {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
if (this.indexing) {
|
||||
if (await this.getIsIndexing()) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
const index = this.getIndexForSearch();
|
||||
const index = await this.getIndexForSearch();
|
||||
if (index == null) {
|
||||
// Fall back to basic search if index is not available
|
||||
return this.searchCiphersBasic(ciphers, query);
|
||||
@@ -230,8 +305,24 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
return sendsMatched.concat(lowPriorityMatched);
|
||||
}
|
||||
|
||||
getIndexForSearch(): lunr.Index {
|
||||
return this.index;
|
||||
async getIndexForSearch(): Promise<lunr.Index | null> {
|
||||
return await firstValueFrom(this.index$);
|
||||
}
|
||||
|
||||
private async setIndexForSearch(index: SerializedLunrIndex): Promise<void> {
|
||||
await this.searchIndexState.update(() => index);
|
||||
}
|
||||
|
||||
private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise<void> {
|
||||
await this.searchIndexEntityIdState.update(() => indexedEntityId);
|
||||
}
|
||||
|
||||
private async setIsIndexing(indexing: boolean): Promise<void> {
|
||||
await this.searchIsIndexingState.update(() => indexing);
|
||||
}
|
||||
|
||||
private async getIsIndexing(): Promise<boolean> {
|
||||
return await firstValueFrom(this.searchIsIndexing$);
|
||||
}
|
||||
|
||||
private fieldExtractor(c: CipherView, joined: boolean) {
|
||||
|
||||
@@ -91,7 +91,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
|
||||
if (userId == null || userId === currentUserId) {
|
||||
this.searchService.clearIndex();
|
||||
await this.searchService.clearIndex();
|
||||
await this.folderService.clearCache();
|
||||
await this.collectionService.clearActiveUserCache();
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ export type CollectionId = Opaque<string, "CollectionId">;
|
||||
export type ProviderId = Opaque<string, "ProviderId">;
|
||||
export type PolicyId = Opaque<string, "PolicyId">;
|
||||
export type CipherId = Opaque<string, "CipherId">;
|
||||
export type IndexedEntityId = Opaque<string, "IndexedEntityId">;
|
||||
|
||||
@@ -89,9 +89,9 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
if (this.searchService != null) {
|
||||
if (value == null) {
|
||||
this.searchService.clearIndex();
|
||||
await this.searchService.clearIndex();
|
||||
} else {
|
||||
this.searchService.indexCiphers(value);
|
||||
await this.searchService.indexCiphers(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,9 +333,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private async reindexCiphers() {
|
||||
const userId = await this.stateService.getUserId();
|
||||
const reindexRequired =
|
||||
this.searchService != null && (this.searchService.indexedEntityId ?? userId) !== userId;
|
||||
this.searchService != null &&
|
||||
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
|
||||
if (reindexRequired) {
|
||||
this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user