1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 00:33:44 +00:00

Separate StateService in background/visualizations

Visualizer state services share storages with background page, which
nicely emulates mv3 synchronization through session/local storage. There
should not be multithreading issues since all of these services are
still running through a single thread, we just now have multiple places
we are reading/writing data from.

Smaller improvements
* Rename browser's state service to BrowserStateService
* Remove unused WithPrototype decorator :celebrate:
* Removed conversion on withPrototypeForArrayMembers. It's reasonable to
think that if the type is maintained, it doesn't need conversion.

Eventually, we should be able to remove the withPrototypeForArrayMembers
decorator as well, but that will require a bit more work on
(de)serialization of the Accounts.data property.
This commit is contained in:
Matt Gibson
2022-11-16 19:23:59 -05:00
parent 862bf28348
commit 3e4817b2b1
6 changed files with 94 additions and 80 deletions

View File

@@ -93,6 +93,7 @@ import AutofillService from "../services/autofill.service";
import { BrowserEnvironmentService } from "../services/browser-environment.service"; import { BrowserEnvironmentService } from "../services/browser-environment.service";
import { BrowserFolderService } from "../services/browser-folder.service"; import { BrowserFolderService } from "../services/browser-folder.service";
import { BrowserPolicyService } from "../services/browser-policy.service"; import { BrowserPolicyService } from "../services/browser-policy.service";
import { BrowserStateService } from "../services/browser-state.service";
import { BrowserCryptoService } from "../services/browserCrypto.service"; import { BrowserCryptoService } from "../services/browserCrypto.service";
import BrowserLocalStorageService from "../services/browserLocalStorage.service"; import BrowserLocalStorageService from "../services/browserLocalStorage.service";
import BrowserMessagingService from "../services/browserMessaging.service"; import BrowserMessagingService from "../services/browserMessaging.service";
@@ -101,7 +102,6 @@ import BrowserPlatformUtilsService from "../services/browserPlatformUtils.servic
import I18nService from "../services/i18n.service"; import I18nService from "../services/i18n.service";
import { KeyGenerationService } from "../services/keyGeneration.service"; import { KeyGenerationService } from "../services/keyGeneration.service";
import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service"; import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service";
import { StateService } from "../services/state.service";
import { VaultFilterService } from "../services/vaultFilter.service"; import { VaultFilterService } from "../services/vaultFilter.service";
import VaultTimeoutService from "../services/vaultTimeout/vaultTimeout.service"; import VaultTimeoutService from "../services/vaultTimeout/vaultTimeout.service";
@@ -227,7 +227,7 @@ export default class MainBackground {
this.secureStorageService, this.secureStorageService,
new StateFactory(GlobalState, Account) new StateFactory(GlobalState, Account)
); );
this.stateService = new StateService( this.stateService = new BrowserStateService(
this.storageService, this.storageService,
this.secureStorageService, this.secureStorageService,
this.memoryStorageService, this.memoryStorageService,

View File

@@ -2,7 +2,7 @@ import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state"; import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { Account } from "../../models/account"; import { Account } from "../../models/account";
import { StateService } from "../../services/state.service"; import { BrowserStateService } from "../../services/browser-state.service";
import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
@@ -34,15 +34,15 @@ export type StateServiceInitOptions = StateServiceFactoryOptions &
StateMigrationServiceInitOptions; StateMigrationServiceInitOptions;
export async function stateServiceFactory( export async function stateServiceFactory(
cache: { stateService?: StateService } & CachedServices, cache: { stateService?: BrowserStateService } & CachedServices,
opts: StateServiceInitOptions opts: StateServiceInitOptions
): Promise<StateService> { ): Promise<BrowserStateService> {
const service = await factory( const service = await factory(
cache, cache,
"stateService", "stateService",
opts, opts,
async () => async () =>
await new StateService( await new BrowserStateService(
await diskStorageServiceFactory(cache, opts), await diskStorageServiceFactory(cache, opts),
await secureStorageServiceFactory(cache, opts), await secureStorageServiceFactory(cache, opts),
await memoryStorageServiceFactory(cache, opts), await memoryStorageServiceFactory(cache, opts),

View File

@@ -38,6 +38,7 @@ import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abs
import { SendService } from "@bitwarden/common/abstractions/send.service"; import { SendService } from "@bitwarden/common/abstractions/send.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { TokenService } from "@bitwarden/common/abstractions/token.service"; import { TokenService } from "@bitwarden/common/abstractions/token.service";
@@ -47,6 +48,8 @@ import { UserVerificationService } from "@bitwarden/common/abstractions/userVeri
import { UsernameGenerationService } from "@bitwarden/common/abstractions/usernameGeneration.service"; import { UsernameGenerationService } from "@bitwarden/common/abstractions/usernameGeneration.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { AuthService } from "@bitwarden/common/services/auth.service"; import { AuthService } from "@bitwarden/common/services/auth.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { LoginService } from "@bitwarden/common/services/login.service"; import { LoginService } from "@bitwarden/common/services/login.service";
@@ -54,10 +57,12 @@ import { SearchService } from "@bitwarden/common/services/search.service";
import MainBackground from "../../background/main.background"; import MainBackground from "../../background/main.background";
import { BrowserApi } from "../../browser/browserApi"; import { BrowserApi } from "../../browser/browserApi";
import { Account } from "../../models/account";
import { AutofillService } from "../../services/abstractions/autofill.service"; import { AutofillService } from "../../services/abstractions/autofill.service";
import { StateService as StateServiceAbstraction } from "../../services/abstractions/state.service"; import { StateService as StateServiceAbstraction } from "../../services/abstractions/state.service";
import { BrowserEnvironmentService } from "../../services/browser-environment.service"; import { BrowserEnvironmentService } from "../../services/browser-environment.service";
import { BrowserPolicyService } from "../../services/browser-policy.service"; import { BrowserPolicyService } from "../../services/browser-policy.service";
import { BrowserStateService } from "../../services/browser-state.service";
import { BrowserFileDownloadService } from "../../services/browserFileDownloadService"; import { BrowserFileDownloadService } from "../../services/browserFileDownloadService";
import BrowserMessagingService from "../../services/browserMessaging.service"; import BrowserMessagingService from "../../services/browserMessaging.service";
import BrowserMessagingPrivateModePopupService from "../../services/browserMessagingPrivateModePopup.service"; import BrowserMessagingPrivateModePopupService from "../../services/browserMessagingPrivateModePopup.service";
@@ -299,10 +304,36 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: getBgService<AbstractStorageService>("memoryStorageService"), useFactory: getBgService<AbstractStorageService>("memoryStorageService"),
}, },
{ {
provide: StateServiceAbstraction, provide: StateMigrationService,
useFactory: getBgService<StateServiceAbstraction>("stateService"), useFactory: getBgService<StateMigrationService>("stateMigrationService"),
deps: [], deps: [],
}, },
{
provide: StateServiceAbstraction,
useFactory: (
storageService: AbstractStorageService,
secureStorageService: AbstractStorageService,
memoryStorageService: AbstractStorageService,
logService: LogServiceAbstraction,
stateMigrationService: StateMigrationService
) => {
return new BrowserStateService(
storageService,
secureStorageService,
memoryStorageService,
logService,
stateMigrationService,
new StateFactory(GlobalState, Account)
);
},
deps: [
AbstractStorageService,
SECURE_STORAGE,
MEMORY_STORAGE,
LogServiceAbstraction,
StateMigrationService,
],
},
{ {
provide: UsernameGenerationService, provide: UsernameGenerationService,
useFactory: getBgService<UsernameGenerationService>("usernameGenerationService"), useFactory: getBgService<UsernameGenerationService>("usernameGenerationService"),
@@ -323,17 +354,19 @@ function getBgService<T>(service: keyof MainBackground) {
}, },
{ {
provide: AbstractThemingService, provide: AbstractThemingService,
useFactory: () => { useFactory: (
stateService: StateServiceAbstraction,
platformUtilsService: PlatformUtilsService
) => {
return new ThemingService( return new ThemingService(
getBgService<StateServiceAbstraction>("stateService")(), stateService,
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light. // Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
// In Safari we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed. // In Safari we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
getBgService<PlatformUtilsService>("platformUtilsService")().isSafari() platformUtilsService.isSafari() ? getBgService<Window>("backgroundWindow")() : window,
? getBgService<Window>("backgroundWindow")()
: window,
document document
); );
}, },
deps: [StateServiceAbstraction, PlatformUtilsService],
}, },
], ],
}) })

View File

@@ -1,6 +1,6 @@
// eslint-disable-next-line no-restricted-imports import { mock, MockProxy } from "jest-mock-extended";
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { import {
MemoryStorageServiceInterface, MemoryStorageServiceInterface,
@@ -18,28 +18,29 @@ import { BrowserComponentState } from "../models/browserComponentState";
import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState"; import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState";
import { BrowserSendComponentState } from "../models/browserSendComponentState"; import { BrowserSendComponentState } from "../models/browserSendComponentState";
import { AbstractKeyGenerationService } from "./abstractions/abstractKeyGeneration.service";
import { BrowserStateService } from "./browser-state.service";
import { LocalBackedSessionStorageService } from "./localBackedSessionStorage.service"; import { LocalBackedSessionStorageService } from "./localBackedSessionStorage.service";
import { StateService } from "./state.service";
describe("Browser State Service", () => { describe("Browser State Service", () => {
let secureStorageService: SubstituteOf<AbstractStorageService>; let secureStorageService: MockProxy<AbstractStorageService>;
let diskStorageService: SubstituteOf<AbstractStorageService>; let diskStorageService: MockProxy<AbstractStorageService>;
let logService: SubstituteOf<LogService>; let logService: MockProxy<LogService>;
let stateMigrationService: SubstituteOf<StateMigrationService>; let stateMigrationService: MockProxy<StateMigrationService>;
let stateFactory: SubstituteOf<StateFactory<GlobalState, Account>>; let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
let useAccountCache: boolean; let useAccountCache: boolean;
let state: State<GlobalState, Account>; let state: State<GlobalState, Account>;
const userId = "userId"; const userId = "userId";
let sut: StateService; let sut: BrowserStateService;
beforeEach(() => { beforeEach(() => {
secureStorageService = Substitute.for(); secureStorageService = mock();
diskStorageService = Substitute.for(); diskStorageService = mock();
logService = Substitute.for(); logService = mock();
stateMigrationService = Substitute.for(); stateMigrationService = mock();
stateFactory = Substitute.for(); stateFactory = mock();
useAccountCache = true; useAccountCache = true;
state = new State(new GlobalState()); state = new State(new GlobalState());
@@ -54,9 +55,12 @@ describe("Browser State Service", () => {
beforeEach(() => { beforeEach(() => {
// We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass. // We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass.
memoryStorageService = Object.create(LocalBackedSessionStorageService.prototype); memoryStorageService = new LocalBackedSessionStorageService(
mock<EncryptService>(),
mock<AbstractKeyGenerationService>()
);
sut = new StateService( sut = new BrowserStateService(
diskStorageService, diskStorageService,
secureStorageService, secureStorageService,
memoryStorageService, memoryStorageService,
@@ -80,14 +84,14 @@ describe("Browser State Service", () => {
}); });
describe("state methods", () => { describe("state methods", () => {
let memoryStorageService: SubstituteOf<AbstractStorageService & MemoryStorageServiceInterface>; let memoryStorageService: MockProxy<AbstractStorageService & MemoryStorageServiceInterface>;
beforeEach(() => { beforeEach(() => {
memoryStorageService = Substitute.for(); memoryStorageService = mock();
const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state))); const stateGetter = (key: string) => Promise.resolve(state);
memoryStorageService.get("state", Arg.any()).mimicks(stateGetter); memoryStorageService.get.mockImplementation(stateGetter);
sut = new StateService( sut = new BrowserStateService(
diskStorageService, diskStorageService,
secureStorageService, secureStorageService,
memoryStorageService, memoryStorageService,
@@ -128,6 +132,7 @@ describe("Browser State Service", () => {
[SendType.Text, 5], [SendType.Text, 5],
]); ]);
state.accounts[userId].send = sendState; state.accounts[userId].send = sendState;
(global as any)["watch"] = state;
const actual = await sut.getBrowserSendComponentState(); const actual = await sut.getBrowserSendComponentState();
expect(actual).toBeInstanceOf(BrowserSendComponentState); expect(actual).toBeInstanceOf(BrowserSendComponentState);

View File

@@ -1,13 +1,12 @@
import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service"; import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service";
import { GlobalState } from "@bitwarden/common/models/domain/global-state"; import { GlobalState } from "@bitwarden/common/models/domain/global-state";
import { StorageOptions } from "@bitwarden/common/models/domain/storage-options"; import { StorageOptions } from "@bitwarden/common/models/domain/storage-options";
import { import { StateService as BaseStateService } from "@bitwarden/common/services/state.service";
StateService as BaseStateService,
withPrototype,
} from "@bitwarden/common/services/state.service";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
import { Account } from "../models/account"; import { Account } from "../models/account";
import { BrowserComponentState } from "../models/browserComponentState"; import { BrowserComponentState } from "../models/browserComponentState";
import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState"; import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState";
@@ -15,10 +14,27 @@ import { BrowserSendComponentState } from "../models/browserSendComponentState";
import { StateService as StateServiceAbstraction } from "./abstractions/state.service"; import { StateService as StateServiceAbstraction } from "./abstractions/state.service";
export class StateService @browserSession
export class BrowserStateService
extends BaseStateService<GlobalState, Account> extends BaseStateService<GlobalState, Account>
implements StateServiceAbstraction implements StateServiceAbstraction
{ {
@sessionSync({
initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account
initializeAsRecord: true,
})
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
@sessionSync({ ctor: String })
protected activeAccountSubject: BehaviorSubject<string>;
@sessionSync({ ctor: Boolean })
protected activeAccountUnlockedSubject: BehaviorSubject<boolean>;
protected accountDeserializer = Account.fromJSON;
async hasInSessionMemory(key: string): Promise<boolean> {
return await this.memoryStorageService.has(key);
}
async getFromSessionMemory<T>(key: string, deserializer?: (obj: Jsonify<T>) => T): Promise<T> { async getFromSessionMemory<T>(key: string, deserializer?: (obj: Jsonify<T>) => T): Promise<T> {
return this.memoryStorageService instanceof AbstractCachedStorageService return this.memoryStorageService instanceof AbstractCachedStorageService
? await this.memoryStorageService.getBypassCache<T>(key, { deserializer: deserializer }) ? await this.memoryStorageService.getBypassCache<T>(key, { deserializer: deserializer })
@@ -44,7 +60,6 @@ export class StateService
); );
} }
@withPrototype(BrowserGroupingsComponentState)
async getBrowserGroupingComponentState( async getBrowserGroupingComponentState(
options?: StorageOptions options?: StorageOptions
): Promise<BrowserGroupingsComponentState> { ): Promise<BrowserGroupingsComponentState> {
@@ -67,7 +82,6 @@ export class StateService
); );
} }
@withPrototype(BrowserComponentState)
async getBrowserCipherComponentState(options?: StorageOptions): Promise<BrowserComponentState> { async getBrowserCipherComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@@ -88,7 +102,6 @@ export class StateService
); );
} }
@withPrototype(BrowserSendComponentState)
async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> { async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@@ -109,7 +122,6 @@ export class StateService
); );
} }
@withPrototype(BrowserComponentState)
async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> { async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))

View File

@@ -2772,42 +2772,6 @@ export class StateService<
} }
} }
export function withPrototype<T>(
constructor: new (...args: any[]) => T,
converter: (input: any) => T = (i) => i
): (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => { value: (...args: any[]) => Promise<T> } {
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
return {
value: function (...args: any[]) {
const originalResult: Promise<T> = originalMethod.apply(this, args);
if (!(originalResult instanceof Promise)) {
throw new Error(
`Error applying prototype to stored value -- result is not a promise for method ${String(
propertyKey
)}`
);
}
return originalResult.then((result) => {
return result == null ||
result.constructor.name === constructor.prototype.constructor.name
? converter(result as T)
: converter(
Object.create(constructor.prototype, Object.getOwnPropertyDescriptors(result)) as T
);
});
},
};
};
}
function withPrototypeForArrayMembers<T>( function withPrototypeForArrayMembers<T>(
memberConstructor: new (...args: any[]) => T, memberConstructor: new (...args: any[]) => T,
memberConverter: (input: any) => T = (i) => i memberConverter: (input: any) => T = (i) => i
@@ -2844,7 +2808,7 @@ function withPrototypeForArrayMembers<T>(
return result.map((r) => { return result.map((r) => {
return r == null || return r == null ||
r.constructor.name === memberConstructor.prototype.constructor.name r.constructor.name === memberConstructor.prototype.constructor.name
? memberConverter(r) ? r
: memberConverter( : memberConverter(
Object.create(memberConstructor.prototype, Object.getOwnPropertyDescriptors(r)) Object.create(memberConstructor.prototype, Object.getOwnPropertyDescriptors(r))
); );