mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
Remove memory storage cache from derived state. Use observable cache and port messaging (#8939)
This commit is contained in:
@@ -490,7 +490,7 @@ export default class MainBackground {
|
||||
this.accountService,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider);
|
||||
this.derivedStateProvider = new BackgroundDerivedStateProvider();
|
||||
this.stateProvider = new DefaultStateProvider(
|
||||
this.activeUserStateProvider,
|
||||
this.singleUserStateProvider,
|
||||
|
||||
@@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state";
|
||||
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
|
||||
|
||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||
import {
|
||||
StorageServiceProviderInitOptions,
|
||||
storageServiceProviderFactory,
|
||||
} from "./storage-service-provider.factory";
|
||||
|
||||
type DerivedStateProviderFactoryOptions = FactoryOptions;
|
||||
|
||||
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions &
|
||||
StorageServiceProviderInitOptions;
|
||||
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions;
|
||||
|
||||
export async function derivedStateProviderFactory(
|
||||
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
|
||||
@@ -21,7 +16,6 @@ export async function derivedStateProviderFactory(
|
||||
cache,
|
||||
"derivedStateProvider",
|
||||
opts,
|
||||
async () =>
|
||||
new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)),
|
||||
async () => new BackgroundDerivedStateProvider(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
||||
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||
@@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider
|
||||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
dependencies: TDeps,
|
||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
||||
): DerivedState<TTo> {
|
||||
const [location, storageService] = storageLocation;
|
||||
return new BackgroundDerivedState(
|
||||
parentState$,
|
||||
deriveDefinition,
|
||||
storageService,
|
||||
deriveDefinition.buildCacheKey(location),
|
||||
deriveDefinition.buildCacheKey(),
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Observable, Subscription } from "rxjs";
|
||||
import { Observable, Subscription, concatMap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
||||
import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state";
|
||||
@@ -22,11 +19,10 @@ export class BackgroundDerivedState<
|
||||
constructor(
|
||||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
memoryStorage: AbstractStorageService & ObservableStorageService,
|
||||
portName: string,
|
||||
dependencies: TDeps,
|
||||
) {
|
||||
super(parentState$, deriveDefinition, memoryStorage, dependencies);
|
||||
super(parentState$, deriveDefinition, dependencies);
|
||||
|
||||
// listen for foreground derived states to connect
|
||||
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
||||
@@ -42,7 +38,20 @@ export class BackgroundDerivedState<
|
||||
});
|
||||
port.onMessage.addListener(listenerCallback);
|
||||
|
||||
const stateSubscription = this.state$.subscribe();
|
||||
const stateSubscription = this.state$
|
||||
.pipe(
|
||||
concatMap(async (state) => {
|
||||
await this.sendMessage(
|
||||
{
|
||||
action: "nextState",
|
||||
data: JSON.stringify(state),
|
||||
id: Utils.newGuid(),
|
||||
},
|
||||
port,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.portSubscriptions.set(port, stateSubscription);
|
||||
});
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
*/
|
||||
|
||||
import { NgZone } from "@angular/core";
|
||||
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
|
||||
import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec/utils";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
|
||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
||||
import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec";
|
||||
|
||||
import { mockPorts } from "../../../spec/mock-port.spec-util";
|
||||
|
||||
@@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory");
|
||||
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
|
||||
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
||||
deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
||||
cleanupDelayMs: 1000,
|
||||
});
|
||||
|
||||
// Mock out the runInsideAngular operator so we don't have to deal with zone.js
|
||||
@@ -35,7 +35,6 @@ describe("foreground background derived state interactions", () => {
|
||||
let foreground: ForegroundDerivedState<Date>;
|
||||
let background: BackgroundDerivedState<string, Date, Record<string, unknown>>;
|
||||
let parentState$: Subject<string>;
|
||||
let memoryStorage: FakeStorageService;
|
||||
const initialParent = "2020-01-01";
|
||||
const ngZone = mock<NgZone>();
|
||||
const portName = "testPort";
|
||||
@@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => {
|
||||
beforeEach(() => {
|
||||
mockPorts();
|
||||
parentState$ = new Subject<string>();
|
||||
memoryStorage = new FakeStorageService();
|
||||
|
||||
background = new BackgroundDerivedState(
|
||||
parentState$,
|
||||
deriveDefinition,
|
||||
memoryStorage,
|
||||
portName,
|
||||
{},
|
||||
);
|
||||
foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
||||
background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {});
|
||||
foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => {
|
||||
});
|
||||
|
||||
it("should initialize a late-connected foreground", async () => {
|
||||
const newForeground = new ForegroundDerivedState(
|
||||
deriveDefinition,
|
||||
memoryStorage,
|
||||
portName,
|
||||
ngZone,
|
||||
);
|
||||
const backgroundEmissions = trackEmissions(background.state$);
|
||||
const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||
const backgroundTracker = new ObservableTracker(background.state$);
|
||||
parentState$.next(initialParent);
|
||||
await awaitAsync();
|
||||
const foregroundTracker = new ObservableTracker(newForeground.state$);
|
||||
|
||||
const foregroundEmissions = trackEmissions(newForeground.state$);
|
||||
await awaitAsync(10);
|
||||
|
||||
expect(backgroundEmissions).toEqual([new Date(initialParent)]);
|
||||
expect(foregroundEmissions).toEqual([new Date(initialParent)]);
|
||||
expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||
expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||
});
|
||||
|
||||
describe("forceValue", () => {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { NgZone } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
||||
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||
@@ -14,23 +9,17 @@ import { DerivedStateDependencies } from "@bitwarden/common/src/types/state";
|
||||
import { ForegroundDerivedState } from "./foreground-derived-state";
|
||||
|
||||
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
|
||||
constructor(
|
||||
storageServiceProvider: StorageServiceProvider,
|
||||
private ngZone: NgZone,
|
||||
) {
|
||||
super(storageServiceProvider);
|
||||
constructor(private ngZone: NgZone) {
|
||||
super();
|
||||
}
|
||||
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||
_parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
_dependencies: TDeps,
|
||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
||||
): DerivedState<TTo> {
|
||||
const [location, storageService] = storageLocation;
|
||||
return new ForegroundDerivedState(
|
||||
deriveDefinition,
|
||||
storageService,
|
||||
deriveDefinition.buildCacheKey(location),
|
||||
deriveDefinition.buildCacheKey(),
|
||||
this.ngZone,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/**
|
||||
* need to update test environment so structuredClone works appropriately
|
||||
* @jest-environment ../../libs/shared/test.environment.ts
|
||||
*/
|
||||
|
||||
import { NgZone } from "@angular/core";
|
||||
import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec";
|
||||
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
|
||||
import { awaitAsync } from "@bitwarden/common/../spec";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||
@@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => {
|
||||
|
||||
describe("ForegroundDerivedState", () => {
|
||||
let sut: ForegroundDerivedState<Date>;
|
||||
let memoryStorage: FakeStorageService;
|
||||
const portName = "testPort";
|
||||
const ngZone = mock<NgZone>();
|
||||
|
||||
beforeEach(() => {
|
||||
memoryStorage = new FakeStorageService();
|
||||
memoryStorage.internalUpdateValuesRequireDeserialization(true);
|
||||
mockPorts();
|
||||
sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
||||
sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => {
|
||||
expect(disconnectSpy).toHaveBeenCalled();
|
||||
expect(sut["port"]).toBeNull();
|
||||
});
|
||||
|
||||
it("should emit when the memory storage updates", async () => {
|
||||
const dateString = "2020-01-01";
|
||||
const emissions = trackEmissions(sut.state$);
|
||||
|
||||
await memoryStorage.save(deriveDefinition.storageKey, {
|
||||
derived: true,
|
||||
value: new Date(dateString),
|
||||
});
|
||||
|
||||
await awaitAsync();
|
||||
|
||||
expect(emissions).toEqual([new Date(dateString)]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,19 +6,14 @@ import {
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
merge,
|
||||
of,
|
||||
share,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
} from "rxjs";
|
||||
import { Jsonify, JsonObject } from "type-fest";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||
import { DerivedStateDependencies } from "@bitwarden/common/types/state";
|
||||
@@ -27,41 +22,28 @@ import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||
import { runInsideAngular } from "../browser/run-inside-angular.operator";
|
||||
|
||||
export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
||||
private storageKey: string;
|
||||
private port: chrome.runtime.Port;
|
||||
private backgroundResponses$: Observable<DerivedStateMessage>;
|
||||
state$: Observable<TTo>;
|
||||
|
||||
constructor(
|
||||
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
|
||||
private memoryStorage: AbstractStorageService & ObservableStorageService,
|
||||
private portName: string,
|
||||
private ngZone: NgZone,
|
||||
) {
|
||||
this.storageKey = deriveDefinition.storageKey;
|
||||
|
||||
const initialStorageGet$ = defer(() => {
|
||||
return this.getStoredValue();
|
||||
}).pipe(
|
||||
filter((s) => s.derived),
|
||||
map((s) => s.value),
|
||||
);
|
||||
|
||||
const latestStorage$ = this.memoryStorage.updates$.pipe(
|
||||
filter((s) => s.key === this.storageKey),
|
||||
switchMap(async (storageUpdate) => {
|
||||
if (storageUpdate.updateType === "remove") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.getStoredValue();
|
||||
}),
|
||||
filter((s) => s?.derived === true), // A "remove" storage update will return us null
|
||||
map((s) => s.value),
|
||||
);
|
||||
const latestValueFromPort$ = (port: chrome.runtime.Port) => {
|
||||
return fromChromeEvent(port.onMessage).pipe(
|
||||
map(([message]) => message as DerivedStateMessage),
|
||||
filter((message) => message.originator === "background" && message.action === "nextState"),
|
||||
map((message) => {
|
||||
const json = JSON.parse(message.data) as Jsonify<TTo>;
|
||||
return this.deriveDefinition.deserialize(json);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
this.state$ = defer(() => of(this.initializePort())).pipe(
|
||||
switchMap(() => merge(initialStorageGet$, latestStorage$)),
|
||||
switchMap(() => latestValueFromPort$(this.port)),
|
||||
share({
|
||||
connector: () => new ReplaySubject<TTo>(1),
|
||||
resetOnRefCountZero: () =>
|
||||
@@ -130,28 +112,4 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
||||
this.port = null;
|
||||
this.backgroundResponses$ = null;
|
||||
}
|
||||
|
||||
protected async getStoredValue(): Promise<{ derived: boolean; value: TTo | null }> {
|
||||
if (this.memoryStorage.valuesRequireDeserialization) {
|
||||
const storedJson = await this.memoryStorage.get<
|
||||
Jsonify<{ derived: true; value: JsonObject }>
|
||||
>(this.storageKey);
|
||||
|
||||
if (!storedJson?.derived) {
|
||||
return { derived: false, value: null };
|
||||
}
|
||||
|
||||
const value = this.deriveDefinition.deserialize(storedJson.value as any);
|
||||
|
||||
return { derived: true, value };
|
||||
} else {
|
||||
const stored = await this.memoryStorage.get<{ derived: true; value: TTo }>(this.storageKey);
|
||||
|
||||
if (!stored?.derived) {
|
||||
return { derived: false, value: null };
|
||||
}
|
||||
|
||||
return { derived: true, value: stored.value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,7 +473,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: DerivedStateProvider,
|
||||
useClass: ForegroundDerivedStateProvider,
|
||||
deps: [StorageServiceProvider, NgZone],
|
||||
deps: [NgZone],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AutofillSettingsServiceAbstraction,
|
||||
|
||||
@@ -314,7 +314,7 @@ export class Main {
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
|
||||
this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider);
|
||||
this.derivedStateProvider = new DefaultDerivedStateProvider();
|
||||
|
||||
this.stateProvider = new DefaultStateProvider(
|
||||
this.activeUserStateProvider,
|
||||
|
||||
@@ -157,7 +157,7 @@ export class Main {
|
||||
activeUserStateProvider,
|
||||
singleUserStateProvider,
|
||||
globalStateProvider,
|
||||
new DefaultDerivedStateProvider(storageServiceProvider),
|
||||
new DefaultDerivedStateProvider(),
|
||||
);
|
||||
|
||||
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
||||
|
||||
Reference in New Issue
Block a user