1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 14:23:32 +00:00

Use Memory Storage directly in Session Sync (#4423)

* Use Memory Storage directly in Session Sync

* Update apps/browser/src/decorators/session-sync-observable/browser-session.decorator.spec.ts

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Fix up test

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
Matt Gibson
2023-01-12 15:39:33 -05:00
committed by GitHub
parent 508979df89
commit 23897ae5fb
17 changed files with 146 additions and 154 deletions

View File

@@ -28,7 +28,10 @@ import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstractions/send.service"; import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstractions/send.service";
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction"; import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
@@ -123,7 +126,7 @@ export default class MainBackground {
messagingService: MessagingServiceAbstraction; messagingService: MessagingServiceAbstraction;
storageService: AbstractStorageService; storageService: AbstractStorageService;
secureStorageService: AbstractStorageService; secureStorageService: AbstractStorageService;
memoryStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService;
i18nService: I18nServiceAbstraction; i18nService: I18nServiceAbstraction;
platformUtilsService: PlatformUtilsServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction;
logService: LogServiceAbstraction; logService: LogServiceAbstraction;

View File

@@ -1,4 +1,7 @@
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { BrowserApi } from "../../browser/browserApi"; import { BrowserApi } from "../../browser/browserApi";
@@ -35,9 +38,9 @@ export function secureStorageServiceFactory(
} }
export function memoryStorageServiceFactory( export function memoryStorageServiceFactory(
cache: { memoryStorageService?: AbstractStorageService } & CachedServices, cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices,
opts: MemoryStorageServiceInitOptions opts: MemoryStorageServiceInitOptions
): Promise<AbstractStorageService> { ): Promise<AbstractMemoryStorageService> {
return factory(cache, "memoryStorageService", opts, async () => { return factory(cache, "memoryStorageService", opts, async () => {
if (BrowserApi.manifestVersion === 3) { if (BrowserApi.manifestVersion === 3) {
return new LocalBackedSessionStorageService( return new LocalBackedSessionStorageService(

View File

@@ -1,10 +0,0 @@
import { BrowserStateService } from "../services/abstractions/browser-state.service";
const clearClipboardStorageKey = "clearClipboardTime";
export const getClearClipboardTime = async (stateService: BrowserStateService) => {
return await stateService.getFromSessionMemory<number>(clearClipboardStorageKey);
};
export const setClearClipboardTime = async (stateService: BrowserStateService, time: number) => {
await stateService.setInSessionMemory(clearClipboardStorageKey, time);
};

View File

@@ -1,5 +1,8 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { AbstractMemoryStorageService } from "@bitwarden/common/abstractions/storage.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { BrowserStateService } from "../../services/browser-state.service"; import { BrowserStateService } from "../../services/browser-state.service";
import { browserSession } from "./browser-session.decorator"; import { browserSession } from "./browser-session.decorator";
@@ -11,18 +14,24 @@ import { sessionSync } from "./session-sync.decorator";
jest.mock("./session-syncer"); jest.mock("./session-syncer");
describe("browserSession decorator", () => { describe("browserSession decorator", () => {
it("should throw if StateService is not a constructor argument", () => { it("should throw if neither StateService nor MemoryStorageService is a constructor argument", () => {
@browserSession @browserSession
class TestClass {} class TestClass {}
expect(() => { expect(() => {
new TestClass(); new TestClass();
}).toThrowError( }).toThrowError(
"Cannot decorate TestClass with browserSession, Browser's StateService must be injected" "Cannot decorate TestClass with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters"
); );
}); });
it("should create if StateService is a constructor argument", () => { it("should create if StateService is a constructor argument", () => {
const stateService = Object.create(BrowserStateService.prototype, {}); const stateService = Object.create(BrowserStateService.prototype, {
memoryStorageService: {
value: Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
}),
},
});
@browserSession @browserSession
class TestClass { class TestClass {
@@ -32,15 +41,28 @@ describe("browserSession decorator", () => {
expect(new TestClass(stateService)).toBeDefined(); expect(new TestClass(stateService)).toBeDefined();
}); });
it("should create if MemoryStorageService is a constructor argument", () => {
const memoryStorageService = Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
});
@browserSession
class TestClass {
constructor(private memoryStorageService: AbstractMemoryStorageService) {}
}
expect(new TestClass(memoryStorageService)).toBeDefined();
});
describe("interaction with @sessionSync decorator", () => { describe("interaction with @sessionSync decorator", () => {
let stateService: BrowserStateService; let memoryStorageService: MemoryStorageService;
@browserSession @browserSession
class TestClass { class TestClass {
@sessionSync({ initializer: (s: string) => s }) @sessionSync({ initializer: (s: string) => s })
private behaviorSubject = new BehaviorSubject(""); private behaviorSubject = new BehaviorSubject("");
constructor(private stateService: BrowserStateService) {} constructor(private memoryStorageService: MemoryStorageService) {}
fromJSON(json: any) { fromJSON(json: any) {
this.behaviorSubject.next(json); this.behaviorSubject.next(json);
@@ -48,16 +70,18 @@ describe("browserSession decorator", () => {
} }
beforeEach(() => { beforeEach(() => {
stateService = Object.create(BrowserStateService.prototype, {}) as BrowserStateService; memoryStorageService = Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
});
}); });
it("should create a session syncer", () => { it("should create a session syncer", () => {
const testClass = new TestClass(stateService) as any as SessionStorable; const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
expect(testClass.__sessionSyncers.length).toEqual(1); expect(testClass.__sessionSyncers.length).toEqual(1);
}); });
it("should initialize the session syncer", () => { it("should initialize the session syncer", () => {
const testClass = new TestClass(stateService) as any as SessionStorable; const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled(); expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { Constructor } from "type-fest"; import { Constructor } from "type-fest";
import { BrowserStateService } from "../../services/browser-state.service"; import { AbstractMemoryStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SessionStorable } from "./session-storable"; import { SessionStorable } from "./session-storable";
import { SessionSyncer } from "./session-syncer"; import { SessionSyncer } from "./session-syncer";
@@ -22,32 +22,51 @@ export function browserSession<TCtor extends Constructor<any>>(constructor: TCto
super(...args); super(...args);
// Require state service to be injected // Require state service to be injected
const stateService: BrowserStateService = [this as any] const storageService: AbstractMemoryStorageService = this.findStorageService(
.concat(args) [this as any].concat(args)
.find( );
(arg) =>
typeof arg.setInSessionMemory === "function" &&
typeof arg.getFromSessionMemory === "function"
);
if (!stateService) {
throw new Error(
`Cannot decorate ${constructor.name} with browserSession, Browser's StateService must be injected`
);
}
if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) { if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) {
return; return;
} }
this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) => this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) =>
this.buildSyncer(metadata, stateService) this.buildSyncer(metadata, storageService)
); );
} }
buildSyncer(metadata: SyncedItemMetadata, stateService: BrowserStateService) { buildSyncer(metadata: SyncedItemMetadata, storageSerice: AbstractMemoryStorageService) {
const syncer = new SessionSyncer((this as any)[metadata.propertyKey], stateService, metadata); const syncer = new SessionSyncer(
(this as any)[metadata.propertyKey],
storageSerice,
metadata
);
syncer.init(); syncer.init();
return syncer; return syncer;
} }
findStorageService(args: any[]): AbstractMemoryStorageService {
const storageService = args.find(this.isMemoryStorageService);
if (storageService) {
return storageService;
}
const stateService = args.find(
(arg) =>
arg?.memoryStorageService != null && this.isMemoryStorageService(arg.memoryStorageService)
);
if (stateService) {
return stateService.memoryStorageService;
}
throw new Error(
`Cannot decorate ${constructor.name} with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters`
);
}
isMemoryStorageService(arg: any): arg is AbstractMemoryStorageService {
return arg.type != null && arg.type === AbstractMemoryStorageService.TYPE;
}
}; };
} }

View File

@@ -1,9 +1,10 @@
import { awaitAsync, awaitAsync as flushAsyncObservables } from "@bitwarden/angular/../test-utils"; import { awaitAsync } from "@bitwarden/angular/../test-utils";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, ReplaySubject } from "rxjs"; import { BehaviorSubject, ReplaySubject } from "rxjs";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { BrowserApi } from "../../browser/browserApi"; import { BrowserApi } from "../../browser/browserApi";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import { SessionSyncer } from "./session-syncer"; import { SessionSyncer } from "./session-syncer";
import { SyncedItemMetadata } from "./sync-item-metadata"; import { SyncedItemMetadata } from "./sync-item-metadata";
@@ -17,7 +18,7 @@ describe("session syncer", () => {
initializer: (s: string) => s, initializer: (s: string) => s,
initializeAs: "object", initializeAs: "object",
}; };
let stateService: MockProxy<BrowserStateService>; let storageService: MockProxy<MemoryStorageService>;
let sut: SessionSyncer; let sut: SessionSyncer;
let behaviorSubject: BehaviorSubject<string>; let behaviorSubject: BehaviorSubject<string>;
@@ -29,9 +30,9 @@ describe("session syncer", () => {
manifest_version: 3, manifest_version: 3,
}); });
stateService = mock<BrowserStateService>(); storageService = mock();
stateService.hasInSessionMemory.mockResolvedValue(false); storageService.has.mockResolvedValue(false);
sut = new SessionSyncer(behaviorSubject, stateService, metaData); sut = new SessionSyncer(behaviorSubject, storageService, metaData);
}); });
afterEach(() => { afterEach(() => {
@@ -43,13 +44,13 @@ describe("session syncer", () => {
describe("constructor", () => { describe("constructor", () => {
it("should throw if subject is not an instance of Subject", () => { it("should throw if subject is not an instance of Subject", () => {
expect(() => { expect(() => {
new SessionSyncer({} as any, stateService, null); new SessionSyncer({} as any, storageService, null);
}).toThrowError("subject must inherit from Subject"); }).toThrowError("subject must inherit from Subject");
}); });
it("should create if either ctor or initializer is provided", () => { it("should create if either ctor or initializer is provided", () => {
expect( expect(
new SessionSyncer(behaviorSubject, stateService, { new SessionSyncer(behaviorSubject, storageService, {
propertyKey, propertyKey,
sessionKey, sessionKey,
ctor: String, ctor: String,
@@ -57,7 +58,7 @@ describe("session syncer", () => {
}) })
).toBeDefined(); ).toBeDefined();
expect( expect(
new SessionSyncer(behaviorSubject, stateService, { new SessionSyncer(behaviorSubject, storageService, {
propertyKey, propertyKey,
sessionKey, sessionKey,
initializer: (s: any) => s, initializer: (s: any) => s,
@@ -67,7 +68,7 @@ describe("session syncer", () => {
}); });
it("should throw if neither ctor or initializer is provided", () => { it("should throw if neither ctor or initializer is provided", () => {
expect(() => { expect(() => {
new SessionSyncer(behaviorSubject, stateService, { new SessionSyncer(behaviorSubject, storageService, {
propertyKey, propertyKey,
sessionKey, sessionKey,
initializeAs: "object", initializeAs: "object",
@@ -82,7 +83,7 @@ describe("session syncer", () => {
replaySubject.next("1"); replaySubject.next("1");
replaySubject.next("2"); replaySubject.next("2");
replaySubject.next("3"); replaySubject.next("3");
sut = new SessionSyncer(replaySubject, stateService, metaData); sut = new SessionSyncer(replaySubject, storageService, metaData);
// block observing the subject // block observing the subject
jest.spyOn(sut as any, "observe").mockImplementation(); jest.spyOn(sut as any, "observe").mockImplementation();
@@ -93,7 +94,7 @@ describe("session syncer", () => {
it("should ignore BehaviorSubject's initial value", () => { it("should ignore BehaviorSubject's initial value", () => {
const behaviorSubject = new BehaviorSubject<string>("initial"); const behaviorSubject = new BehaviorSubject<string>("initial");
sut = new SessionSyncer(behaviorSubject, stateService, metaData); sut = new SessionSyncer(behaviorSubject, storageService, metaData);
// block observing the subject // block observing the subject
jest.spyOn(sut as any, "observe").mockImplementation(); jest.spyOn(sut as any, "observe").mockImplementation();
@@ -103,7 +104,7 @@ describe("session syncer", () => {
}); });
it("should grab an initial value from storage if it exists", async () => { it("should grab an initial value from storage if it exists", async () => {
stateService.hasInSessionMemory.mockResolvedValue(true); storageService.has.mockResolvedValue(true);
//Block a call to update //Block a call to update
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation(); const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
@@ -114,7 +115,7 @@ describe("session syncer", () => {
}); });
it("should not grab an initial value from storage if it does not exist", async () => { it("should not grab an initial value from storage if it does not exist", async () => {
stateService.hasInSessionMemory.mockResolvedValue(false); storageService.has.mockResolvedValue(false);
//Block a call to update //Block a call to update
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation(); const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
@@ -139,8 +140,8 @@ describe("session syncer", () => {
it("should update the session memory", async () => { it("should update the session memory", async () => {
// await finishing of fire-and-forget operation // await finishing of fire-and-forget operation
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
expect(stateService.setInSessionMemory).toHaveBeenCalledTimes(1); expect(storageService.save).toHaveBeenCalledTimes(1);
expect(stateService.setInSessionMemory).toHaveBeenCalledWith(sessionKey, "test"); expect(storageService.save).toHaveBeenCalledWith(sessionKey, "test");
}); });
it("should update sessionSyncers in other contexts", async () => { it("should update sessionSyncers in other contexts", async () => {
@@ -170,27 +171,29 @@ describe("session syncer", () => {
it("should ignore messages with the wrong command", async () => { it("should ignore messages with the wrong command", async () => {
await sut.updateFromMessage({ command: "wrong_command", id: sut.id }); await sut.updateFromMessage({ command: "wrong_command", id: sut.id });
expect(stateService.getFromSessionMemory).not.toHaveBeenCalled(); expect(storageService.getBypassCache).not.toHaveBeenCalled();
expect(nextSpy).not.toHaveBeenCalled(); expect(nextSpy).not.toHaveBeenCalled();
}); });
it("should ignore messages from itself", async () => { it("should ignore messages from itself", async () => {
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id }); await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id });
expect(stateService.getFromSessionMemory).not.toHaveBeenCalled(); expect(storageService.getBypassCache).not.toHaveBeenCalled();
expect(nextSpy).not.toHaveBeenCalled(); expect(nextSpy).not.toHaveBeenCalled();
}); });
it("should update from message on emit from another instance", async () => { it("should update from message on emit from another instance", async () => {
const builder = jest.fn(); const builder = jest.fn();
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
stateService.getFromSessionMemory.mockResolvedValue("test"); storageService.getBypassCache.mockResolvedValue("test");
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" }); await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" });
await flushAsyncObservables(); await awaitAsync();
expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1); expect(storageService.getBypassCache).toHaveBeenCalledTimes(1);
expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey, builder); expect(storageService.getBypassCache).toHaveBeenCalledWith(sessionKey, {
deserializer: builder,
});
expect(nextSpy).toHaveBeenCalledTimes(1); expect(nextSpy).toHaveBeenCalledTimes(1);
expect(nextSpy).toHaveBeenCalledWith("test"); expect(nextSpy).toHaveBeenCalledWith("test");

View File

@@ -1,9 +1,9 @@
import { BehaviorSubject, concatMap, ReplaySubject, Subject, Subscription } from "rxjs"; import { BehaviorSubject, concatMap, ReplaySubject, Subject, Subscription } from "rxjs";
import { AbstractMemoryStorageService } from "@bitwarden/common/abstractions/storage.service";
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
import { BrowserApi } from "../../browser/browserApi"; import { BrowserApi } from "../../browser/browserApi";
import { BrowserStateService } from "../../services/abstractions/browser-state.service";
import { SyncedItemMetadata } from "./sync-item-metadata"; import { SyncedItemMetadata } from "./sync-item-metadata";
@@ -16,7 +16,7 @@ export class SessionSyncer {
constructor( constructor(
private subject: Subject<any>, private subject: Subject<any>,
private stateService: BrowserStateService, private memoryStorageService: AbstractMemoryStorageService,
private metaData: SyncedItemMetadata private metaData: SyncedItemMetadata
) { ) {
if (!(subject instanceof Subject)) { if (!(subject instanceof Subject)) {
@@ -43,7 +43,7 @@ export class SessionSyncer {
this.observe(); this.observe();
// must be synchronous // must be synchronous
this.stateService.hasInSessionMemory(this.metaData.sessionKey).then((hasInSessionMemory) => { this.memoryStorageService.has(this.metaData.sessionKey).then((hasInSessionMemory) => {
if (hasInSessionMemory) { if (hasInSessionMemory) {
this.update(); this.update();
} }
@@ -86,13 +86,15 @@ export class SessionSyncer {
async update() { async update() {
const builder = SyncedItemMetadata.builder(this.metaData); const builder = SyncedItemMetadata.builder(this.metaData);
const value = await this.stateService.getFromSessionMemory(this.metaData.sessionKey, builder); const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey, {
deserializer: builder,
});
this.ignoreNUpdates = 1; this.ignoreNUpdates = 1;
this.subject.next(value); this.subject.next(value);
} }
private async updateSession(value: any) { private async updateSession(value: any) {
await this.stateService.setInSessionMemory(this.metaData.sessionKey, value); await this.memoryStorageService.save(this.metaData.sessionKey, value);
await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id }); await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id });
} }

View File

@@ -40,7 +40,10 @@ 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 { StateMigrationService } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import {
AbstractMemoryStorageService,
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";
import { TotpService } from "@bitwarden/common/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service";
@@ -329,7 +332,7 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: ( useFactory: (
storageService: AbstractStorageService, storageService: AbstractStorageService,
secureStorageService: AbstractStorageService, secureStorageService: AbstractStorageService,
memoryStorageService: AbstractStorageService, memoryStorageService: AbstractMemoryStorageService,
logService: LogServiceAbstraction, logService: LogServiceAbstraction,
stateMigrationService: StateMigrationService stateMigrationService: StateMigrationService
) => { ) => {

View File

@@ -1,5 +1,3 @@
import { Jsonify } from "type-fest";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StorageOptions } from "@bitwarden/common/models/domain/storage-options"; import { StorageOptions } from "@bitwarden/common/models/domain/storage-options";
@@ -9,9 +7,6 @@ import { BrowserGroupingsComponentState } from "../../models/browserGroupingsCom
import { BrowserSendComponentState } from "../../models/browserSendComponentState"; import { BrowserSendComponentState } from "../../models/browserSendComponentState";
export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> { export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> {
abstract hasInSessionMemory(key: string): Promise<boolean>;
abstract getFromSessionMemory<T>(key: string, deserializer?: (obj: Jsonify<T>) => T): Promise<T>;
abstract setInSessionMemory(key: string, value: any): Promise<void>;
getBrowserGroupingComponentState: ( getBrowserGroupingComponentState: (
options?: StorageOptions options?: StorageOptions
) => Promise<BrowserGroupingsComponentState>; ) => Promise<BrowserGroupingsComponentState>;

View File

@@ -1,9 +1,8 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
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, AbstractMemoryStorageService,
AbstractStorageService, AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service"; } from "@bitwarden/common/abstractions/storage.service";
import { SendType } from "@bitwarden/common/enums/sendType"; import { SendType } from "@bitwarden/common/enums/sendType";
@@ -18,9 +17,10 @@ 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 { BrowserStateService } from "./browser-state.service";
import { LocalBackedSessionStorageService } from "./localBackedSessionStorage.service";
// disable session syncing to just test class
jest.mock("../decorators/session-sync-observable/");
describe("Browser State Service", () => { describe("Browser State Service", () => {
let secureStorageService: MockProxy<AbstractStorageService>; let secureStorageService: MockProxy<AbstractStorageService>;
@@ -50,41 +50,8 @@ describe("Browser State Service", () => {
state.activeUserId = userId; state.activeUserId = userId;
}); });
describe("direct memory storage access", () => {
let memoryStorageService: LocalBackedSessionStorageService;
beforeEach(() => {
// We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass.
memoryStorageService = new LocalBackedSessionStorageService(
mock<EncryptService>(),
mock<AbstractKeyGenerationService>()
);
sut = new BrowserStateService(
diskStorageService,
secureStorageService,
memoryStorageService,
logService,
stateMigrationService,
stateFactory,
useAccountCache
);
});
it("should bypass cache if possible", async () => {
const spyBypass = jest
.spyOn(memoryStorageService, "getBypassCache")
.mockResolvedValue("value");
const spyGet = jest.spyOn(memoryStorageService, "get");
const result = await sut.getFromSessionMemory("key");
expect(spyBypass).toHaveBeenCalled();
expect(spyGet).not.toHaveBeenCalled();
expect(result).toBe("value");
});
});
describe("state methods", () => { describe("state methods", () => {
let memoryStorageService: MockProxy<AbstractStorageService & MemoryStorageServiceInterface>; let memoryStorageService: MockProxy<AbstractMemoryStorageService>;
beforeEach(() => { beforeEach(() => {
memoryStorageService = mock(); memoryStorageService = mock();

View File

@@ -1,7 +1,5 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
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 { StateService as BaseStateService } from "@bitwarden/common/services/state.service"; import { StateService as BaseStateService } from "@bitwarden/common/services/state.service";
@@ -36,20 +34,6 @@ export class BrowserStateService
protected accountDeserializer = Account.fromJSON; 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> {
return this.memoryStorageService instanceof AbstractCachedStorageService
? await this.memoryStorageService.getBypassCache<T>(key, { deserializer: deserializer })
: await this.memoryStorageService.get<T>(key);
}
async setInSessionMemory(key: string, value: any): Promise<void> {
await this.memoryStorageService.save(key, value);
}
async addAccount(account: Account) { async addAccount(account: Account) {
// Apply browser overrides to default account values // Apply browser overrides to default account values
account = new Account(account); account = new Account(account);

View File

@@ -1,10 +1,7 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { import { AbstractMemoryStorageService } from "@bitwarden/common/abstractions/storage.service";
AbstractCachedStorageService,
MemoryStorageServiceInterface,
} from "@bitwarden/common/abstractions/storage.service";
import { EncString } from "@bitwarden/common/models/domain/enc-string"; import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { MemoryStorageOptions } from "@bitwarden/common/models/domain/storage-options"; import { MemoryStorageOptions } from "@bitwarden/common/models/domain/storage-options";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
@@ -21,10 +18,7 @@ const keys = {
sessionKey: "session", sessionKey: "session",
}; };
export class LocalBackedSessionStorageService export class LocalBackedSessionStorageService extends AbstractMemoryStorageService {
extends AbstractCachedStorageService
implements MemoryStorageServiceInterface
{
private cache = new Map<string, unknown>(); private cache = new Map<string, unknown>();
private localStorage = new BrowserLocalStorageService(); private localStorage = new BrowserLocalStorageService();
private sessionStorage = new BrowserMemoryStorageService(); private sessionStorage = new BrowserMemoryStorageService();

View File

@@ -8,7 +8,10 @@ import {
} from "@bitwarden/angular/services/injection-tokens"; } from "@bitwarden/angular/services/injection-tokens";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { StateMigrationService } from "@bitwarden/common/abstractions/stateMigration.service"; import { StateMigrationService } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { CipherData } from "@bitwarden/common/models/data/cipher.data"; import { CipherData } from "@bitwarden/common/models/data/cipher.data";
import { CollectionData } from "@bitwarden/common/models/data/collection.data"; import { CollectionData } from "@bitwarden/common/models/data/collection.data";
@@ -25,7 +28,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
constructor( constructor(
storageService: AbstractStorageService, storageService: AbstractStorageService,
@Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService, @Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService,
@Inject(MEMORY_STORAGE) memoryStorageService: AbstractStorageService, @Inject(MEMORY_STORAGE) memoryStorageService: AbstractMemoryStorageService,
logService: LogService, logService: LogService,
stateMigrationService: StateMigrationService, stateMigrationService: StateMigrationService,
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>, @Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,

View File

@@ -1,10 +1,13 @@
import { InjectionToken } from "@angular/core"; import { InjectionToken } from "@angular/core";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { StateFactory } from "@bitwarden/common/factories/stateFactory";
export const WINDOW = new InjectionToken<Window>("WINDOW"); export const WINDOW = new InjectionToken<Window>("WINDOW");
export const MEMORY_STORAGE = new InjectionToken<AbstractStorageService>("MEMORY_STORAGE"); export const MEMORY_STORAGE = new InjectionToken<AbstractMemoryStorageService>("MEMORY_STORAGE");
export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE"); export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY"); export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE"); export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");

View File

@@ -7,10 +7,11 @@ export abstract class AbstractStorageService {
abstract remove(key: string, options?: StorageOptions): Promise<void>; abstract remove(key: string, options?: StorageOptions): Promise<void>;
} }
export abstract class AbstractCachedStorageService extends AbstractStorageService { export abstract class AbstractMemoryStorageService extends AbstractStorageService {
// Used to identify the service in the session sync decorator framework
static readonly TYPE = "MemoryStorageService";
readonly type = AbstractMemoryStorageService.TYPE;
abstract get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
abstract getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>; abstract getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
} }
export interface MemoryStorageServiceInterface {
get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
}

View File

@@ -1,12 +1,6 @@
import { import { AbstractMemoryStorageService } from "../abstractions/storage.service";
AbstractStorageService,
MemoryStorageServiceInterface,
} from "../abstractions/storage.service";
export class MemoryStorageService export class MemoryStorageService extends AbstractMemoryStorageService {
extends AbstractStorageService
implements MemoryStorageServiceInterface
{
private store = new Map<string, any>(); private store = new Map<string, any>();
get<T>(key: string): Promise<T> { get<T>(key: string): Promise<T> {
@@ -33,4 +27,8 @@ export class MemoryStorageService
this.store.delete(key); this.store.delete(key);
return Promise.resolve(); return Promise.resolve();
} }
getBypassCache<T>(key: string): Promise<T> {
return this.get<T>(key);
}
} }

View File

@@ -5,7 +5,7 @@ import { LogService } from "../abstractions/log.service";
import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
import { StateMigrationService } from "../abstractions/stateMigration.service"; import { StateMigrationService } from "../abstractions/stateMigration.service";
import { import {
MemoryStorageServiceInterface, AbstractMemoryStorageService,
AbstractStorageService, AbstractStorageService,
} from "../abstractions/storage.service"; } from "../abstractions/storage.service";
import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
@@ -87,7 +87,7 @@ export class StateService<
constructor( constructor(
protected storageService: AbstractStorageService, protected storageService: AbstractStorageService,
protected secureStorageService: AbstractStorageService, protected secureStorageService: AbstractStorageService,
protected memoryStorageService: AbstractStorageService & MemoryStorageServiceInterface, protected memoryStorageService: AbstractMemoryStorageService,
protected logService: LogService, protected logService: LogService,
protected stateMigrationService: StateMigrationService, protected stateMigrationService: StateMigrationService,
protected stateFactory: StateFactory<TGlobalState, TAccount>, protected stateFactory: StateFactory<TGlobalState, TAccount>,