1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +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

@@ -6,6 +6,10 @@ import {
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "../../../platform/background/service-factories/global-state-provider.factory";
import {
LogServiceInitOptions,
logServiceFactory,
@@ -19,7 +23,8 @@ type AccountServiceFactoryOptions = FactoryOptions;
export type AccountServiceInitOptions = AccountServiceFactoryOptions &
MessagingServiceInitOptions &
LogServiceInitOptions;
LogServiceInitOptions &
GlobalStateProviderInitOptions;
export function accountServiceFactory(
cache: { accountService?: AccountService } & CachedServices,
@@ -32,7 +37,8 @@ export function accountServiceFactory(
async () =>
new AccountServiceImplementation(
await messagingServiceFactory(cache, opts),
await logServiceFactory(cache, opts)
await logServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts)
)
);
}

View File

@@ -60,9 +60,11 @@ import { ContainerService } from "@bitwarden/common/platform/services/container.
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
@@ -145,6 +147,7 @@ import BrowserPlatformUtilsService from "../platform/services/browser-platform-u
import { BrowserStateService } from "../platform/services/browser-state.service";
import { KeyGenerationService } from "../platform/services/key-generation.service";
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { BrowserSendService } from "../services/browser-send.service";
import { BrowserSettingsService } from "../services/browser-settings.service";
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
@@ -225,6 +228,7 @@ export default class MainBackground {
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
accountService: AccountServiceAbstraction;
globalStateProvider: GlobalStateProvider;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
backgroundWindow = window;
@@ -279,8 +283,16 @@ export default class MainBackground {
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
new KeyGenerationService(this.cryptoFunctionService)
)
: new MemoryStorageService();
this.accountService = new AccountServiceImplementation(this.messagingService, this.logService);
: new BackgroundMemoryStorageService();
this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageService as BackgroundMemoryStorageService,
this.storageService as BrowserLocalStorageService
);
this.accountService = new AccountServiceImplementation(
this.messagingService,
this.logService,
this.globalStateProvider
);
this.stateService = new BrowserStateService(
this.storageService,
this.secureStorageService,

View File

@@ -0,0 +1,33 @@
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
DiskStorageServiceInitOptions,
MemoryStorageServiceInitOptions,
observableDiskStorageServiceFactory,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
type GlobalStateProviderFactoryOptions = FactoryOptions;
export type GlobalStateProviderInitOptions = GlobalStateProviderFactoryOptions &
MemoryStorageServiceInitOptions &
DiskStorageServiceInitOptions;
export async function globalStateProviderFactory(
cache: { globalStateProvider?: GlobalStateProvider } & CachedServices,
opts: GlobalStateProviderInitOptions
): Promise<GlobalStateProvider> {
return factory(
cache,
"globalStateProvider",
opts,
async () =>
new DefaultGlobalStateProvider(
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts)
)
);
}

View File

@@ -1,12 +1,14 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { BrowserApi } from "../../browser/browser-api";
import BrowserLocalStorageService from "../../services/browser-local-storage.service";
import { LocalBackedSessionStorageService } from "../../services/local-backed-session-storage.service";
import { BackgroundMemoryStorageService } from "../../storage/background-memory-storage.service";
import { EncryptServiceInitOptions, encryptServiceFactory } from "./encrypt-service.factory";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
@@ -29,6 +31,14 @@ export function diskStorageServiceFactory(
): Promise<AbstractStorageService> {
return factory(cache, "diskStorageService", opts, () => new BrowserLocalStorageService());
}
export function observableDiskStorageServiceFactory(
cache: {
diskStorageService?: AbstractStorageService & ObservableStorageService;
} & CachedServices,
opts: DiskStorageServiceInitOptions
): Promise<AbstractStorageService & ObservableStorageService> {
return factory(cache, "diskStorageService", opts, () => new BrowserLocalStorageService());
}
export function secureStorageServiceFactory(
cache: { secureStorageService?: AbstractStorageService } & CachedServices,
@@ -51,3 +61,14 @@ export function memoryStorageServiceFactory(
return new MemoryStorageService();
});
}
export function observableMemoryStorageServiceFactory(
cache: {
memoryStorageService?: AbstractMemoryStorageService & ObservableStorageService;
} & CachedServices,
opts: MemoryStorageServiceInitOptions
): Promise<AbstractMemoryStorageService & ObservableStorageService> {
return factory(cache, "memoryStorageService", opts, async () => {
return new BackgroundMemoryStorageService();
});
}

View File

@@ -1,21 +1,20 @@
import { Observable, mergeMap } from "rxjs";
import { mergeMap } from "rxjs";
import {
AbstractStorageService,
StorageUpdate,
ObservableStorageService,
StorageUpdateType,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { fromChromeEvent } from "../../browser/from-chrome-event";
export default abstract class AbstractChromeStorageService implements AbstractStorageService {
constructor(protected chromeStorageApi: chrome.storage.StorageArea) {}
export default abstract class AbstractChromeStorageService
implements AbstractStorageService, ObservableStorageService
{
updates$;
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$(): Observable<StorageUpdate> {
return fromChromeEvent(this.chromeStorageApi.onChanged).pipe(
constructor(protected chromeStorageApi: chrome.storage.StorageArea) {
this.updates$ = fromChromeEvent(this.chromeStorageApi.onChanged).pipe(
mergeMap(([changes]) => {
return Object.entries(changes).map(([key, change]) => {
// The `newValue` property isn't on the StorageChange object
@@ -37,6 +36,10 @@ export default abstract class AbstractChromeStorageService implements AbstractSt
);
}
get valuesRequireDeserialization(): boolean {
return true;
}
async get<T>(key: string): Promise<T> {
return new Promise((resolve) => {
this.chromeStorageApi.get(key, (obj: any) => {

View File

@@ -27,22 +27,20 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
private localStorage = new BrowserLocalStorageService();
private sessionStorage = new BrowserMemoryStorageService();
private updatesSubject = new Subject<StorageUpdate>();
updates$;
constructor(
private encryptService: EncryptService,
private keyGenerationService: AbstractKeyGenerationService
) {
super();
this.updates$ = this.updatesSubject.asObservable();
}
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$() {
return this.updatesSubject.asObservable();
}
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
if (this.cache.has(key)) {
return this.cache.get(key) as T;

View File

@@ -0,0 +1,78 @@
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { BrowserApi } from "../browser/browser-api";
import { MemoryStoragePortMessage } from "./port-messages";
import { portName } from "./port-name";
export class BackgroundMemoryStorageService extends MemoryStorageService {
private _ports: chrome.runtime.Port[] = [];
constructor() {
super();
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
if (port.name !== portName(chrome.storage.session)) {
return;
}
this._ports.push(port);
const listenerCallback = this.onMessageFromForeground.bind(this);
port.onDisconnect.addListener(() => {
this._ports.splice(this._ports.indexOf(port), 1);
port.onMessage.removeListener(listenerCallback);
});
port.onMessage.addListener(listenerCallback);
// Initialize the new memory storage service with existing data
this.sendMessage({
action: "initialization",
data: Array.from(this.store.keys()),
});
});
this.updates$.subscribe((update) => {
this.sendMessage({
action: "subject_update",
data: update,
});
});
}
private async onMessageFromForeground(message: MemoryStoragePortMessage) {
if (message.originator === "background") {
return;
}
let result: unknown = null;
switch (message.action) {
case "get":
case "getBypassCache":
case "has": {
result = await this[message.action](message.key);
break;
}
case "save":
await this.save(message.key, JSON.parse(message.data as string) as unknown);
break;
case "remove":
await this.remove(message.key);
break;
}
this.sendMessage({
id: message.id,
key: message.key,
data: JSON.stringify(result),
});
}
private async sendMessage(data: Omit<MemoryStoragePortMessage, "originator">) {
this._ports.forEach((port) => {
port.postMessage({
...data,
originator: "background",
});
});
}
}

View File

@@ -0,0 +1,113 @@
import { Observable, Subject, filter, firstValueFrom, map } from "rxjs";
import {
AbstractMemoryStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { fromChromeEvent } from "../browser/from-chrome-event";
import { MemoryStoragePortMessage } from "./port-messages";
import { portName } from "./port-name";
export class ForegroundMemoryStorageService extends AbstractMemoryStorageService {
private _port: chrome.runtime.Port;
private _backgroundResponses$: Observable<MemoryStoragePortMessage>;
private updatesSubject = new Subject<StorageUpdate>();
get valuesRequireDeserialization(): boolean {
return true;
}
updates$;
constructor() {
super();
this.updates$ = this.updatesSubject.asObservable();
this._port = chrome.runtime.connect({ name: portName(chrome.storage.session) });
this._backgroundResponses$ = fromChromeEvent(this._port.onMessage).pipe(
map(([message]) => message),
filter((message) => message.originator === "background")
);
this._backgroundResponses$
.pipe(
filter(
(message) => message.action === "subject_update" || message.action === "initialization"
)
)
.subscribe((message) => {
switch (message.action) {
case "initialization":
this.handleInitialize(message.data as string[]); // Map entries as array
break;
case "subject_update":
this.handleSubjectUpdate(message.data as StorageUpdate);
break;
default:
throw new Error(`Unknown action: ${message.action}`);
}
});
}
async get<T>(key: string): Promise<T> {
return await this.delegateToBackground<T>("get", key);
}
async getBypassCache<T>(key: string): Promise<T> {
return await this.delegateToBackground<T>("getBypassCache", key);
}
async has(key: string): Promise<boolean> {
return await this.delegateToBackground<boolean>("has", key);
}
async save<T>(key: string, obj: T): Promise<void> {
await this.delegateToBackground<T>("save", key, obj);
}
async remove(key: string): Promise<void> {
await this.delegateToBackground<void>("remove", key);
}
private async delegateToBackground<T>(
action: MemoryStoragePortMessage["action"],
key: string,
data?: T
): Promise<T> {
const id = Utils.newGuid();
// listen for response before request
const response = firstValueFrom(
this._backgroundResponses$.pipe(
filter((message) => message.id === id),
map((message) => JSON.parse(message.data as string) as T)
)
);
this.sendMessage({
id: id,
key: key,
action: action,
data: JSON.stringify(data),
});
const result = await response;
return result;
}
private sendMessage(data: Omit<MemoryStoragePortMessage, "originator">) {
this._port.postMessage({
...data,
originator: "foreground",
});
}
private handleInitialize(data: string[]) {
// TODO: this isn't a save, but we don't have a better indicator for this
data.forEach((key) => {
this.updatesSubject.next({ key, updateType: "save" });
});
}
private handleSubjectUpdate(data: StorageUpdate) {
this.updatesSubject.next(data);
}
}

View File

@@ -0,0 +1,61 @@
import { trackEmissions } from "@bitwarden/common/../spec/utils";
import { BackgroundMemoryStorageService } from "./background-memory-storage.service";
import { ForegroundMemoryStorageService } from "./foreground-memory-storage.service";
import { mockPort } from "./mock-port.spec-util";
import { portName } from "./port-name";
describe("foreground background memory storage interaction", () => {
let foreground: ForegroundMemoryStorageService;
let background: BackgroundMemoryStorageService;
beforeEach(() => {
mockPort(portName(chrome.storage.session));
background = new BackgroundMemoryStorageService();
foreground = new ForegroundMemoryStorageService();
});
afterEach(() => {
jest.resetAllMocks();
});
test.each(["has", "get", "getBypassCache"])(
"background should respond with the correct value for %s",
async (action: "get" | "has" | "getBypassCache") => {
const key = "key";
const value = "value";
background[action] = jest.fn().mockResolvedValue(value);
const result = await foreground[action](key);
expect(result).toEqual(value);
}
);
test("background should call save from foreground", async () => {
const key = "key";
const value = "value";
const actionSpy = jest.spyOn(background, "save");
await foreground.save(key, value);
expect(actionSpy).toHaveBeenCalledWith(key, value);
});
test("background should call remove from foreground", async () => {
const key = "key";
const actionSpy = jest.spyOn(background, "remove");
await foreground.remove(key);
expect(actionSpy).toHaveBeenCalledWith(key);
});
test("background updates push to foreground", async () => {
const key = "key";
const value = "value";
const updateType = "save";
const emissions = trackEmissions(foreground.updates$);
await background.save(key, value);
expect(emissions).toEqual([{ key, updateType }]);
});
});

View File

@@ -0,0 +1,25 @@
import { mockDeep } from "jest-mock-extended";
/**
* Mocks a chrome.runtime.Port set up to send messages through `postMessage` to `onMessage.addListener` callbacks.
* @returns a mock chrome.runtime.Port
*/
export function mockPort(name: string) {
const port = mockDeep<chrome.runtime.Port>();
// notify listeners of a new port
(chrome.runtime.connect as jest.Mock).mockImplementation((portInfo) => {
port.name = portInfo.name;
(chrome.runtime.onConnect.addListener as jest.Mock).mock.calls.forEach(([callbackFn]) => {
callbackFn(port);
});
return port;
});
// set message broadcast
(port.postMessage as jest.Mock).mockImplementation((message) => {
(port.onMessage.addListener as jest.Mock).mock.calls.forEach(([callbackFn]) => {
callbackFn(message);
});
});
return port;
}

View File

@@ -0,0 +1,20 @@
import {
AbstractMemoryStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
type MemoryStoragePortMessage = {
id?: string;
key?: string;
/**
* We allow sending a string[] array since it is JSON safe and StorageUpdate since it's
* a simple object with just two properties that are strings. Everything else is expected to
* be JSON-ified.
*/
data: string | string[] | StorageUpdate;
originator: "foreground" | "background";
action?:
| keyof Pick<AbstractMemoryStorageService, "get" | "getBypassCache" | "has" | "save" | "remove">
| "subject_update"
| "initialization";
};

View File

@@ -0,0 +1,12 @@
export function portName(storageLocation: chrome.storage.StorageArea) {
switch (storageLocation) {
case chrome.storage.local:
return "local";
case chrome.storage.sync:
return "sync";
case chrome.storage.session:
return "session";
default:
throw new Error("Unknown storage location");
}
}

View File

@@ -1,7 +1,12 @@
import { APP_INITIALIZER, LOCALE_ID, NgModule } from "@angular/core";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
import { MEMORY_STORAGE, SECURE_STORAGE } from "@bitwarden/angular/services/injection-tokens";
import {
MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
SECURE_STORAGE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
@@ -100,9 +105,11 @@ import { BrowserConfigService } from "../../platform/services/browser-config.ser
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service";
import { BrowserI18nService } from "../../platform/services/browser-i18n.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service";
import BrowserMessagingService from "../../platform/services/browser-messaging.service";
import { BrowserStateService } from "../../platform/services/browser-state.service";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { BrowserSendService } from "../../services/browser-send.service";
import { BrowserSettingsService } from "../../services/browser-settings.service";
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
@@ -361,7 +368,7 @@ function getBgService<T>(service: keyof MainBackground) {
},
{
provide: AbstractStorageService,
useFactory: getBgService<AbstractStorageService>("storageService"),
useClass: BrowserLocalStorageService,
deps: [],
},
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
@@ -444,12 +451,20 @@ function getBgService<T>(service: keyof MainBackground) {
{
provide: SECURE_STORAGE,
useFactory: getBgService<AbstractStorageService>("secureStorageService"),
deps: [],
},
{
provide: MEMORY_STORAGE,
useFactory: getBgService<AbstractStorageService>("memoryStorageService"),
},
{
provide: OBSERVABLE_MEMORY_STORAGE,
useClass: ForegroundMemoryStorageService,
deps: [],
},
{
provide: OBSERVABLE_DISK_STORAGE,
useExisting: AbstractStorageService,
},
{
provide: StateServiceAbstraction,
useFactory: (

View File

@@ -25,6 +25,7 @@ const runtime = {
sendMessage: jest.fn(),
getManifest: jest.fn(),
getURL: jest.fn((path) => `chrome-extension://id/${path}`),
connect: jest.fn(),
onConnect: {
addListener: jest.fn(),
},

View File

@@ -45,6 +45,9 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { AuditService } from "@bitwarden/common/services/audit.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
@@ -161,6 +164,7 @@ export class Main {
configApiService: ConfigApiServiceAbstraction;
configService: CliConfigService;
accountService: AccountService;
globalStateProvider: GlobalStateProvider;
constructor() {
let p = null;
@@ -200,7 +204,18 @@ export class Main {
this.memoryStorageService = new MemoryStorageService();
this.accountService = new AccountServiceImplementation(null, this.logService);
this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageService,
this.storageService
);
this.messagingService = new NoopMessagingService();
this.accountService = new AccountServiceImplementation(
this.messagingService,
this.logService,
this.globalStateProvider
);
this.stateService = new StateService(
this.storageService,
@@ -221,7 +236,6 @@ export class Main {
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.stateService);
this.messagingService = new NoopMessagingService();
this.environmentService = new EnvironmentService(this.stateService);
const customUserAgent =

View File

@@ -29,6 +29,7 @@ export class LowdbStorageService implements AbstractStorageService {
private defaults: any;
private ready = false;
private updatesSubject = new Subject<StorageUpdate>();
updates$;
constructor(
protected logService: LogService,
@@ -38,6 +39,7 @@ export class LowdbStorageService implements AbstractStorageService {
private requireLock = false
) {
this.defaults = defaults;
this.updates$ = this.updatesSubject.asObservable();
}
@sequentialize(() => "lowdbStorageInit")
@@ -110,9 +112,6 @@ export class LowdbStorageService implements AbstractStorageService {
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$() {
return this.updatesSubject.asObservable();
}
async get<T>(key: string): Promise<T> {
await this.waitForReady();

View File

@@ -7,6 +7,8 @@ import {
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
MEMORY_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
@@ -102,6 +104,8 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
{ provide: AbstractStorageService, useClass: ElectronRendererStorageService },
{ provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService },
{ provide: MEMORY_STORAGE, useClass: MemoryStorageService },
{ provide: OBSERVABLE_MEMORY_STORAGE, useExisting: MEMORY_STORAGE },
{ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService },
{
provide: SystemServiceAbstraction,
useClass: SystemService,

View File

@@ -6,6 +6,9 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
// eslint-disable-next-line import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { MenuMain } from "./main/menu/menu.main";
import { MessagingMain } from "./main/messaging.main";
@@ -85,6 +88,10 @@ export class Main {
storageDefaults["global.vaultTimeoutAction"] = "lock";
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
this.memoryStorageService = new MemoryStorageService();
const globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageService,
this.storageService
);
// TODO: this state service will have access to on disk storage, but not in memory storage.
// If we could get this to work using the stateService singleton that the rest of the app uses we could save
@@ -95,7 +102,11 @@ export class Main {
this.memoryStorageService,
this.logService,
new StateFactory(GlobalState, Account),
new AccountServiceImplementation(null, this.logService), // will not broadcast logouts. This is a hack until we can remove messaging dependency
new AccountServiceImplementation(
new NoopMessagingService(),
this.logService,
globalStateProvider
), // will not broadcast logouts. This is a hack until we can remove messaging dependency
false // Do not use disk caching because this will get out of sync with the renderer service
);

View File

@@ -11,8 +11,10 @@ export class ElectronRendererStorageService implements AbstractStorageService {
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$() {
return this.updatesSubject.asObservable();
updates$;
constructor() {
this.updates$ = this.updatesSubject.asObservable();
}
get<T>(key: string): Promise<T> {

View File

@@ -40,6 +40,7 @@ type Options = BaseOptions<"get"> | BaseOptions<"has"> | SaveOptions | BaseOptio
export class ElectronStorageService implements AbstractStorageService {
private store: ElectronStore;
private updatesSubject = new Subject<StorageUpdate>();
updates$;
constructor(dir: string, defaults = {}) {
if (!fs.existsSync(dir)) {
@@ -50,6 +51,7 @@ export class ElectronStorageService implements AbstractStorageService {
name: "data",
};
this.store = new Store(storeConfig);
this.updates$ = this.updatesSubject.asObservable();
ipcMain.handle("storageService", (event, options: Options) => {
switch (options.action) {
@@ -68,9 +70,6 @@ export class ElectronStorageService implements AbstractStorageService {
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$() {
return this.updatesSubject.asObservable();
}
get<T>(key: string): Promise<T> {
const val = this.store.get(key) as T;

View File

@@ -8,6 +8,8 @@ import {
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
MEMORY_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
@@ -74,6 +76,8 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
provide: MEMORY_STORAGE,
useClass: MemoryStorageService,
},
{ provide: OBSERVABLE_MEMORY_STORAGE, useExisting: MEMORY_STORAGE },
{ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService },
{
provide: PlatformUtilsServiceAbstraction,
useClass: WebPlatformUtilsService,

View File

@@ -19,8 +19,10 @@ export class HtmlStorageService implements AbstractStorageService {
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$() {
return this.updatesSubject.asObservable();
updates$;
constructor() {
this.updates$ = this.updatesSubject.asObservable();
}
get<T>(key: string, options: StorageOptions = this.defaultOptions): Promise<T> {