1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00

Ps/pm 2910/add browser storage services (#6849)

* Allow for update logic in state update callbacks

* Prefer reading updates to sending in stream

* Inform state providers when they must deserialize

* Update DefaultGlobalState to act more like DefaultUserState

* Fully Implement AbstractStorageService

* Add KeyDefinitionOptions

* Address PR feedback

* Prefer testing interactions for ports

* Synced memory storage for browser

* Fix port handling

* Do not stringify port message data

* Use messaging storage

* Initialize new foreground memory storage services

This will need to be rethought for short-lived background pages, but for
now the background is the source of truth for memory storage

* Use global state for account service

* Use BrowserApi listener to avoid safari memory leaks

* Fix build errors: debugging and missed impls

* Prefer bound arrow functions

* JSON Stringify Messages

* Prefer `useClass`

* Use noop services

* extract storage observable to new interface

This also reverts changes for the existing services to use
foreground/background services. Those are now used only in state
providers

* Fix web DI

* Prefer initializing observable in constructor

* Do not use jsonify as equality operator

* Remove port listener to avoid memory leaks

* Fix logic and type issues

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
Matt Gibson
2023-11-21 16:35:37 -05:00
committed by GitHub
parent 1ecf019397
commit 24c240d0d4
36 changed files with 744 additions and 160 deletions

View File

@@ -1,50 +1,80 @@
import {
BehaviorSubject,
Subject,
combineLatestWith,
map,
distinctUntilChanged,
shareReplay,
} from "rxjs";
import { Subject, combineLatestWith, map, distinctUntilChanged, shareReplay } from "rxjs";
import { Jsonify } from "type-fest";
import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service";
import {
AccountInfo,
InternalAccountService,
accountInfoEqual,
} from "../../auth/abstractions/account.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import {
ACCOUNT_ACCOUNTS,
ACCOUNT_ACTIVE_ACCOUNT_ID,
GlobalState,
GlobalStateProvider,
} from "../../platform/state";
import { UserId } from "../../types/guid";
import { AuthenticationStatus } from "../enums/authentication-status";
export function AccountsDeserializer(
accounts: Jsonify<Record<UserId, AccountInfo> | null>
): Record<UserId, AccountInfo> {
if (accounts == null) {
return {};
}
return accounts;
}
export class AccountServiceImplementation implements InternalAccountService {
private accounts = new BehaviorSubject<Record<UserId, AccountInfo>>({});
private activeAccountId = new BehaviorSubject<UserId | undefined>(undefined);
private lock = new Subject<UserId>();
private logout = new Subject<UserId>();
private accountsState: GlobalState<Record<UserId, AccountInfo>>;
private activeAccountIdState: GlobalState<UserId | undefined>;
accounts$ = this.accounts.asObservable();
activeAccount$ = this.activeAccountId.pipe(
combineLatestWith(this.accounts$),
map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)),
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: false })
);
accounts$;
activeAccount$;
accountLock$ = this.lock.asObservable();
accountLogout$ = this.logout.asObservable();
constructor(private messagingService: MessagingService, private logService: LogService) {}
constructor(
private messagingService: MessagingService,
private logService: LogService,
private globalStateProvider: GlobalStateProvider
) {
this.accountsState = this.globalStateProvider.get(ACCOUNT_ACCOUNTS);
this.activeAccountIdState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID);
this.accounts$ = this.accountsState.state$.pipe(
map((accounts) => (accounts == null ? {} : accounts))
);
this.activeAccount$ = this.activeAccountIdState.state$.pipe(
combineLatestWith(this.accounts$),
map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)),
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: false })
);
}
addAccount(userId: UserId, accountData: AccountInfo): void {
this.accounts.value[userId] = accountData;
this.accounts.next(this.accounts.value);
this.accountsState.update((accounts) => {
accounts ||= {};
accounts[userId] = accountData;
return accounts;
});
}
setAccountName(userId: UserId, name: string): void {
this.setAccountInfo(userId, { ...this.accounts.value[userId], name });
this.setAccountInfo(userId, { name });
}
setAccountEmail(userId: UserId, email: string): void {
this.setAccountInfo(userId, { ...this.accounts.value[userId], email });
this.setAccountInfo(userId, { email });
}
setAccountStatus(userId: UserId, status: AuthenticationStatus): void {
this.setAccountInfo(userId, { ...this.accounts.value[userId], status });
this.setAccountInfo(userId, { status });
if (status === AuthenticationStatus.LoggedOut) {
this.logout.next(userId);
@@ -54,16 +84,22 @@ export class AccountServiceImplementation implements InternalAccountService {
}
switchAccount(userId: UserId) {
if (userId == null) {
// indicates no account is active
this.activeAccountId.next(undefined);
return;
}
this.activeAccountIdState.update(
(_, accounts) => {
if (userId == null) {
// indicates no account is active
return undefined;
}
if (this.accounts.value[userId] == null) {
throw new Error("Account does not exist");
}
this.activeAccountId.next(userId);
if (accounts?.[userId] == null) {
throw new Error("Account does not exist");
}
return userId;
},
{
combineLatestWith: this.accounts$,
}
);
}
// TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow
@@ -76,18 +112,26 @@ export class AccountServiceImplementation implements InternalAccountService {
}
}
private setAccountInfo(userId: UserId, accountInfo: AccountInfo) {
if (this.accounts.value[userId] == null) {
throw new Error("Account does not exist");
private setAccountInfo(userId: UserId, update: Partial<AccountInfo>) {
function newAccountInfo(oldAccountInfo: AccountInfo): AccountInfo {
return { ...oldAccountInfo, ...update };
}
this.accountsState.update(
(accounts) => {
accounts[userId] = newAccountInfo(accounts[userId]);
return accounts;
},
{
// Avoid unnecessary updates
// TODO: Faster comparison, maybe include a hash on the objects?
shouldUpdate: (accounts) => {
if (accounts?.[userId] == null) {
throw new Error("Account does not exist");
}
// Avoid unnecessary updates
// TODO: Faster comparison, maybe include a hash on the objects?
if (JSON.stringify(this.accounts.value[userId]) === JSON.stringify(accountInfo)) {
return;
}
this.accounts.value[userId] = accountInfo;
this.accounts.next(this.accounts.value);
return !accountInfoEqual(accounts[userId], newAccountInfo(accounts[userId]));
},
}
);
}
}