1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-01 09:13:54 +00:00

reduce emissions to vault list

This commit is contained in:
jaasen-livefront
2025-11-19 21:09:53 -08:00
parent 8814eed992
commit 8354238541
3 changed files with 75 additions and 20 deletions

View File

@@ -109,7 +109,10 @@ export class VaultPopupItemsService {
map((a) => a?.id),
filter((userId): userId is UserId => userId != null),
switchMap((userId) =>
merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe(
merge(
this.cipherService.ciphers$(userId),
this.cipherService.ciphersWithLocalData$(userId),
).pipe(
runInsideAngular(this.ngZone),
tap(() => this._ciphersLoading$.next()),
waitUntilSync(this.syncService),
@@ -163,6 +166,7 @@ export class VaultPopupItemsService {
}),
),
),
shareReplay({ refCount: false, bufferSize: 1 }),
);
/**

View File

@@ -32,6 +32,16 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
abstract cipherListViews$(userId: UserId): Observable<CipherListView[] | CipherView[]>;
abstract ciphers$(userId: UserId): Observable<Record<CipherId, CipherData>>;
abstract localData$(userId: UserId): Observable<Record<CipherId, LocalData>>;
/**
* Emits decrypted (or list-view) ciphers merged with their LocalData.
*
* The emitted items must be treated as {@link CipherViewLike},
* since the underlying implementation may emit either `CipherView`
* or `CipherListView` depending on feature flags.
*
* Never emits `null`; always emits an array (empty or populated).
*/
abstract ciphersWithLocalData$(userId: UserId): Observable<CipherViewLike[]>;
/**
* An observable monitoring the add/edit cipher info saved to memory.
*/

View File

@@ -168,6 +168,57 @@ export class CipherService implements CipherServiceAbstraction {
);
});
private cipherListWithoutLocalData$ = perUserCache$((userId: UserId) => {
return this.configService.getFeatureFlag$(FeatureFlag.PM22134SdkCipherListView).pipe(
switchMap((useSdk) => {
if (!useSdk) {
return this.cipherViews$(userId).pipe(
// filter out "null" decrypt-in-progress if it ever appears
filter((ciphers): ciphers is CipherView[] => ciphers != null),
);
}
return combineLatest([
this.encryptedCiphersState(userId).state$,
this.keyService.cipherDecryptionKeys$(userId, true),
]).pipe(
filter(([cipherDataState, keys]) => cipherDataState != null && keys != null),
map(([cipherDataState]) => Object.values(cipherDataState)),
switchMap(async (cipherDataArray) => {
const ciphers = cipherDataArray.map((c) => new Cipher(c));
const [decrypted, failures] = await this.decryptCiphersWithSdk(ciphers, userId, false);
await this.setFailedDecryptedCiphers(failures, userId);
return decrypted;
}),
);
}),
);
});
ciphersWithLocalData$(userId: UserId): Observable<CipherViewLike[]> {
return combineLatest([this.cipherListViews$(userId), this.localData$(userId)]).pipe(
map(([ciphers, localData]) => {
if (!ciphers) {
return [] as CipherViewLike[];
}
return ciphers.map((cipher) => {
const cipherId = uuidAsString(cipher.id) as CipherId;
const local = localData?.[cipherId];
if (!local) {
return cipher as CipherViewLike;
}
return {
...(cipher as CipherViewLike),
localData: local,
} as CipherViewLike;
});
}),
);
}
/**
* Observable that emits an array of decrypted ciphers for the active user.
* This observable will not emit until the encrypted ciphers have either been loaded from state or after sync.
@@ -178,10 +229,9 @@ export class CipherService implements CipherServiceAbstraction {
cipherViews$ = perUserCache$((userId: UserId): Observable<CipherView[] | null> => {
return combineLatest([
this.encryptedCiphersState(userId).state$,
this.localData$(userId),
this.keyService.cipherDecryptionKeys$(userId),
]).pipe(
filter(([ciphers, _, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet
filter(([ciphers, keys]) => ciphers != null && keys != null),
switchMap(() => this.getAllDecrypted(userId)),
tap(() => {
this.messageSender.send("updateOverlayCiphers");
@@ -796,27 +846,18 @@ export class CipherService implements CipherServiceAbstraction {
}
const cipherId = id as CipherId;
if (ciphersLocalData[cipherId]) {
ciphersLocalData[cipherId].lastUsedDate = new Date().getTime();
} else {
ciphersLocalData[cipherId] = { lastUsedDate: new Date().getTime() };
}
const now = new Date().getTime();
await this.localDataState(userId).update(() => ciphersLocalData);
const decryptedCipherCache = await this.getDecryptedCiphers(userId);
if (!decryptedCipherCache) {
if (ciphersLocalData[cipherId]?.lastUsedDate === now) {
return;
}
for (let i = 0; i < decryptedCipherCache.length; i++) {
const cached = decryptedCipherCache[i];
if (cached.id === id) {
cached.localData = ciphersLocalData[id as CipherId];
break;
}
}
await this.setDecryptedCiphers(decryptedCipherCache, userId);
ciphersLocalData[cipherId] = {
...(ciphersLocalData[cipherId] ?? {}),
lastUsedDate: now,
};
await this.localDataState(userId).update(() => ciphersLocalData);
}
async updateLastLaunchedDate(id: string, userId: UserId): Promise<void> {