1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-29 15:53:45 +00:00

[PM-30289] Vault Popup Item Service optimizations

- Add distinctUntilChanged to _otherAutofillTypes$ to prevent duplicate emissions
- debounceTime(0) to _allDecryptedCiphers to avoid back-to-back emissions from ciphers$() and localData$()
- shareReplay() to _allDecryptedCiphers to avoid multiple unique subscriptions
- Add _effectiveSearchText$ to avoid flashing vault list when search query is nonSearchable
- Add distinctUntilChanged to filters$ observable to avoid duplicate emissions during page load
This commit is contained in:
Shane
2026-01-26 12:27:02 -08:00
parent 3228e986af
commit 966e1ae4c0
2 changed files with 26 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ import { toObservable } from "@angular/core/rxjs-interop";
import {
combineLatest,
concatMap,
debounceTime,
distinctUntilChanged,
distinctUntilKeyChanged,
filter,
@@ -99,6 +100,7 @@ export class VaultPopupItemsService {
...(showIdentities ? [CipherType.Identity] : []),
];
}),
distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),
);
/**
@@ -111,6 +113,7 @@ export class VaultPopupItemsService {
filter((userId): userId is UserId => userId != null),
switchMap((userId) =>
merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe(
debounceTime(0),
runInsideAngular(this.ngZone),
tap(() => this._ciphersLoading$.next()),
waitUntilSync(this.syncService),
@@ -164,24 +167,35 @@ export class VaultPopupItemsService {
}),
),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
/**
* Observable that emits the search text when it's searchable, or an empty string when it's not.
* This prevents unnecessary re-renders when typing non-searchable text (e.g., single characters).
* @private
*/
private _effectiveSearchText$ = combineLatest([
this.searchText$,
getUserId(this.accountService.activeAccount$),
]).pipe(
switchMap(async ([searchText, userId]) => {
const isSearchable = await this.searchService.isSearchable(userId, searchText);
return isSearchable ? searchText : "";
}),
distinctUntilChanged(),
shareReplay({ refCount: true, bufferSize: 1 }),
);
/**
* Observable that indicates whether there is search text present that is searchable.
* @private
*/
private _hasSearchText = combineLatest([
this.searchText$,
getUserId(this.accountService.activeAccount$),
]).pipe(
switchMap(([searchText, userId]) => {
return this.searchService.isSearchable(userId, searchText);
}),
);
private _hasSearchText = this._effectiveSearchText$.pipe(map((text) => text !== ""));
private _filteredCipherList$: Observable<PopupCipherViewLike[]> = combineLatest([
this._activeCipherList$,
this.searchText$,
this._effectiveSearchText$,
this.vaultPopupListFiltersService.filterFunction$,
getUserId(this.accountService.activeAccount$),
]).pipe(

View File

@@ -100,6 +100,9 @@ export class VaultPopupListFiltersService {
*/
filters$ = this.filterForm.valueChanges.pipe(
startWith(this.filterForm.value),
distinctUntilChanged(
(previous, current) => JSON.stringify(previous) === JSON.stringify(current),
),
shareReplay({ bufferSize: 1, refCount: true }),
);