mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +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.accountService,
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
);
|
);
|
||||||
this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider);
|
this.derivedStateProvider = new BackgroundDerivedStateProvider();
|
||||||
this.stateProvider = new DefaultStateProvider(
|
this.stateProvider = new DefaultStateProvider(
|
||||||
this.activeUserStateProvider,
|
this.activeUserStateProvider,
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
|
|||||||
@@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state";
|
|||||||
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
|
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
|
||||||
|
|
||||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
import {
|
|
||||||
StorageServiceProviderInitOptions,
|
|
||||||
storageServiceProviderFactory,
|
|
||||||
} from "./storage-service-provider.factory";
|
|
||||||
|
|
||||||
type DerivedStateProviderFactoryOptions = FactoryOptions;
|
type DerivedStateProviderFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions &
|
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions;
|
||||||
StorageServiceProviderInitOptions;
|
|
||||||
|
|
||||||
export async function derivedStateProviderFactory(
|
export async function derivedStateProviderFactory(
|
||||||
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
|
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
|
||||||
@@ -21,7 +16,6 @@ export async function derivedStateProviderFactory(
|
|||||||
cache,
|
cache,
|
||||||
"derivedStateProvider",
|
"derivedStateProvider",
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () => new BackgroundDerivedStateProvider(),
|
||||||
new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import {
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
// 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";
|
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||||
@@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider
|
|||||||
parentState$: Observable<TFrom>,
|
parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
dependencies: TDeps,
|
dependencies: TDeps,
|
||||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
|
||||||
): DerivedState<TTo> {
|
): DerivedState<TTo> {
|
||||||
const [location, storageService] = storageLocation;
|
|
||||||
return new BackgroundDerivedState(
|
return new BackgroundDerivedState(
|
||||||
parentState$,
|
parentState$,
|
||||||
deriveDefinition,
|
deriveDefinition,
|
||||||
storageService,
|
deriveDefinition.buildCacheKey(),
|
||||||
deriveDefinition.buildCacheKey(location),
|
|
||||||
dependencies,
|
dependencies,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Observable, Subscription } from "rxjs";
|
import { Observable, Subscription, concatMap } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import {
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
// 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";
|
import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state";
|
||||||
@@ -22,11 +19,10 @@ export class BackgroundDerivedState<
|
|||||||
constructor(
|
constructor(
|
||||||
parentState$: Observable<TFrom>,
|
parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
memoryStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
portName: string,
|
portName: string,
|
||||||
dependencies: TDeps,
|
dependencies: TDeps,
|
||||||
) {
|
) {
|
||||||
super(parentState$, deriveDefinition, memoryStorage, dependencies);
|
super(parentState$, deriveDefinition, dependencies);
|
||||||
|
|
||||||
// listen for foreground derived states to connect
|
// listen for foreground derived states to connect
|
||||||
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
||||||
@@ -42,7 +38,20 @@ export class BackgroundDerivedState<
|
|||||||
});
|
});
|
||||||
port.onMessage.addListener(listenerCallback);
|
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);
|
this.portSubscriptions.set(port, stateSubscription);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NgZone } from "@angular/core";
|
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 { mock } from "jest-mock-extended";
|
||||||
import { Subject, firstValueFrom } from "rxjs";
|
import { Subject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
|
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
|
||||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-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";
|
import { mockPorts } from "../../../spec/mock-port.spec-util";
|
||||||
|
|
||||||
@@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory");
|
|||||||
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
|
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
|
||||||
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
||||||
deserializer: (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
|
// 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 foreground: ForegroundDerivedState<Date>;
|
||||||
let background: BackgroundDerivedState<string, Date, Record<string, unknown>>;
|
let background: BackgroundDerivedState<string, Date, Record<string, unknown>>;
|
||||||
let parentState$: Subject<string>;
|
let parentState$: Subject<string>;
|
||||||
let memoryStorage: FakeStorageService;
|
|
||||||
const initialParent = "2020-01-01";
|
const initialParent = "2020-01-01";
|
||||||
const ngZone = mock<NgZone>();
|
const ngZone = mock<NgZone>();
|
||||||
const portName = "testPort";
|
const portName = "testPort";
|
||||||
@@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockPorts();
|
mockPorts();
|
||||||
parentState$ = new Subject<string>();
|
parentState$ = new Subject<string>();
|
||||||
memoryStorage = new FakeStorageService();
|
|
||||||
|
|
||||||
background = new BackgroundDerivedState(
|
background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {});
|
||||||
parentState$,
|
foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||||
deriveDefinition,
|
|
||||||
memoryStorage,
|
|
||||||
portName,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize a late-connected foreground", async () => {
|
it("should initialize a late-connected foreground", async () => {
|
||||||
const newForeground = new ForegroundDerivedState(
|
const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||||
deriveDefinition,
|
const backgroundTracker = new ObservableTracker(background.state$);
|
||||||
memoryStorage,
|
|
||||||
portName,
|
|
||||||
ngZone,
|
|
||||||
);
|
|
||||||
const backgroundEmissions = trackEmissions(background.state$);
|
|
||||||
parentState$.next(initialParent);
|
parentState$.next(initialParent);
|
||||||
await awaitAsync();
|
const foregroundTracker = new ObservableTracker(newForeground.state$);
|
||||||
|
|
||||||
const foregroundEmissions = trackEmissions(newForeground.state$);
|
expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||||
await awaitAsync(10);
|
expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||||
|
|
||||||
expect(backgroundEmissions).toEqual([new Date(initialParent)]);
|
|
||||||
expect(foregroundEmissions).toEqual([new Date(initialParent)]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("forceValue", () => {
|
describe("forceValue", () => {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { NgZone } from "@angular/core";
|
import { NgZone } from "@angular/core";
|
||||||
import { Observable } from "rxjs";
|
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";
|
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
// 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";
|
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";
|
import { ForegroundDerivedState } from "./foreground-derived-state";
|
||||||
|
|
||||||
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
|
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
|
||||||
constructor(
|
constructor(private ngZone: NgZone) {
|
||||||
storageServiceProvider: StorageServiceProvider,
|
super();
|
||||||
private ngZone: NgZone,
|
|
||||||
) {
|
|
||||||
super(storageServiceProvider);
|
|
||||||
}
|
}
|
||||||
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||||
_parentState$: Observable<TFrom>,
|
_parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
_dependencies: TDeps,
|
_dependencies: TDeps,
|
||||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
|
||||||
): DerivedState<TTo> {
|
): DerivedState<TTo> {
|
||||||
const [location, storageService] = storageLocation;
|
|
||||||
return new ForegroundDerivedState(
|
return new ForegroundDerivedState(
|
||||||
deriveDefinition,
|
deriveDefinition,
|
||||||
storageService,
|
deriveDefinition.buildCacheKey(),
|
||||||
deriveDefinition.buildCacheKey(location),
|
|
||||||
this.ngZone,
|
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 { NgZone } from "@angular/core";
|
||||||
import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec";
|
import { awaitAsync } from "@bitwarden/common/../spec";
|
||||||
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
|
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||||
@@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => {
|
|||||||
|
|
||||||
describe("ForegroundDerivedState", () => {
|
describe("ForegroundDerivedState", () => {
|
||||||
let sut: ForegroundDerivedState<Date>;
|
let sut: ForegroundDerivedState<Date>;
|
||||||
let memoryStorage: FakeStorageService;
|
|
||||||
const portName = "testPort";
|
const portName = "testPort";
|
||||||
const ngZone = mock<NgZone>();
|
const ngZone = mock<NgZone>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
memoryStorage = new FakeStorageService();
|
|
||||||
memoryStorage.internalUpdateValuesRequireDeserialization(true);
|
|
||||||
mockPorts();
|
mockPorts();
|
||||||
sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => {
|
|||||||
expect(disconnectSpy).toHaveBeenCalled();
|
expect(disconnectSpy).toHaveBeenCalled();
|
||||||
expect(sut["port"]).toBeNull();
|
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,
|
filter,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
merge,
|
|
||||||
of,
|
of,
|
||||||
share,
|
share,
|
||||||
switchMap,
|
switchMap,
|
||||||
tap,
|
tap,
|
||||||
timer,
|
timer,
|
||||||
} from "rxjs";
|
} 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||||
import { DerivedStateDependencies } from "@bitwarden/common/types/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";
|
import { runInsideAngular } from "../browser/run-inside-angular.operator";
|
||||||
|
|
||||||
export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
||||||
private storageKey: string;
|
|
||||||
private port: chrome.runtime.Port;
|
private port: chrome.runtime.Port;
|
||||||
private backgroundResponses$: Observable<DerivedStateMessage>;
|
private backgroundResponses$: Observable<DerivedStateMessage>;
|
||||||
state$: Observable<TTo>;
|
state$: Observable<TTo>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
|
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
|
||||||
private memoryStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
private portName: string,
|
private portName: string,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
) {
|
) {
|
||||||
this.storageKey = deriveDefinition.storageKey;
|
const latestValueFromPort$ = (port: chrome.runtime.Port) => {
|
||||||
|
return fromChromeEvent(port.onMessage).pipe(
|
||||||
const initialStorageGet$ = defer(() => {
|
map(([message]) => message as DerivedStateMessage),
|
||||||
return this.getStoredValue();
|
filter((message) => message.originator === "background" && message.action === "nextState"),
|
||||||
}).pipe(
|
map((message) => {
|
||||||
filter((s) => s.derived),
|
const json = JSON.parse(message.data) as Jsonify<TTo>;
|
||||||
map((s) => s.value),
|
return this.deriveDefinition.deserialize(json);
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
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),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state$ = defer(() => of(this.initializePort())).pipe(
|
this.state$ = defer(() => of(this.initializePort())).pipe(
|
||||||
switchMap(() => merge(initialStorageGet$, latestStorage$)),
|
switchMap(() => latestValueFromPort$(this.port)),
|
||||||
share({
|
share({
|
||||||
connector: () => new ReplaySubject<TTo>(1),
|
connector: () => new ReplaySubject<TTo>(1),
|
||||||
resetOnRefCountZero: () =>
|
resetOnRefCountZero: () =>
|
||||||
@@ -130,28 +112,4 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
|||||||
this.port = null;
|
this.port = null;
|
||||||
this.backgroundResponses$ = 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({
|
safeProvider({
|
||||||
provide: DerivedStateProvider,
|
provide: DerivedStateProvider,
|
||||||
useClass: ForegroundDerivedStateProvider,
|
useClass: ForegroundDerivedStateProvider,
|
||||||
deps: [StorageServiceProvider, NgZone],
|
deps: [NgZone],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AutofillSettingsServiceAbstraction,
|
provide: AutofillSettingsServiceAbstraction,
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ export class Main {
|
|||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider);
|
this.derivedStateProvider = new DefaultDerivedStateProvider();
|
||||||
|
|
||||||
this.stateProvider = new DefaultStateProvider(
|
this.stateProvider = new DefaultStateProvider(
|
||||||
this.activeUserStateProvider,
|
this.activeUserStateProvider,
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export class Main {
|
|||||||
activeUserStateProvider,
|
activeUserStateProvider,
|
||||||
singleUserStateProvider,
|
singleUserStateProvider,
|
||||||
globalStateProvider,
|
globalStateProvider,
|
||||||
new DefaultDerivedStateProvider(storageServiceProvider),
|
new DefaultDerivedStateProvider(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
||||||
|
|||||||
@@ -1047,7 +1047,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: DerivedStateProvider,
|
provide: DerivedStateProvider,
|
||||||
useClass: DefaultDerivedStateProvider,
|
useClass: DefaultDerivedStateProvider,
|
||||||
deps: [StorageServiceProvider],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: StateProvider,
|
provide: StateProvider,
|
||||||
|
|||||||
@@ -249,11 +249,11 @@ export class FakeDerivedStateProvider implements DerivedStateProvider {
|
|||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
dependencies: TDeps,
|
dependencies: TDeps,
|
||||||
): DerivedState<TTo> {
|
): DerivedState<TTo> {
|
||||||
let result = this.states.get(deriveDefinition.buildCacheKey("memory")) as DerivedState<TTo>;
|
let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState<TTo>;
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = new FakeDerivedState(parentState$, deriveDefinition, dependencies);
|
result = new FakeDerivedState(parentState$, deriveDefinition, dependencies);
|
||||||
this.states.set(deriveDefinition.buildCacheKey("memory"), result);
|
this.states.set(deriveDefinition.buildCacheKey(), result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from "./fake-state-provider";
|
|||||||
export * from "./fake-state";
|
export * from "./fake-state";
|
||||||
export * from "./fake-account-service";
|
export * from "./fake-account-service";
|
||||||
export * from "./fake-storage.service";
|
export * from "./fake-storage.service";
|
||||||
|
export * from "./observable-tracker";
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ export class ObservableTracker<T> {
|
|||||||
/**
|
/**
|
||||||
* Awaits the next emission from the observable, or throws if the timeout is exceeded
|
* Awaits the next emission from the observable, or throws if the timeout is exceeded
|
||||||
* @param msTimeout The maximum time to wait for another emission before throwing
|
* @param msTimeout The maximum time to wait for another emission before throwing
|
||||||
|
* @returns The next emission from the observable
|
||||||
|
* @throws If the timeout is exceeded
|
||||||
*/
|
*/
|
||||||
async expectEmission(msTimeout = 50) {
|
async expectEmission(msTimeout = 50): Promise<T> {
|
||||||
await firstValueFrom(
|
return await firstValueFrom(
|
||||||
this.observable.pipe(
|
this.observable.pipe(
|
||||||
timeout({
|
timeout({
|
||||||
first: msTimeout,
|
first: msTimeout,
|
||||||
|
|||||||
@@ -171,8 +171,8 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies
|
|||||||
return this.options.clearOnCleanup ?? true;
|
return this.options.clearOnCleanup ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCacheKey(location: string): string {
|
buildCacheKey(): string {
|
||||||
return `derived_${location}_${this.stateDefinition.name}_${this.uniqueDerivationName}`;
|
return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { DerivedStateDependencies } from "../../../types/state";
|
import { DerivedStateDependencies } from "../../../types/state";
|
||||||
import {
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "../../abstractions/storage.service";
|
|
||||||
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
|
||||||
import { DeriveDefinition } from "../derive-definition";
|
import { DeriveDefinition } from "../derive-definition";
|
||||||
import { DerivedState } from "../derived-state";
|
import { DerivedState } from "../derived-state";
|
||||||
import { DerivedStateProvider } from "../derived-state.provider";
|
import { DerivedStateProvider } from "../derived-state.provider";
|
||||||
@@ -15,18 +10,14 @@ import { DefaultDerivedState } from "./default-derived-state";
|
|||||||
export class DefaultDerivedStateProvider implements DerivedStateProvider {
|
export class DefaultDerivedStateProvider implements DerivedStateProvider {
|
||||||
private cache: Record<string, DerivedState<unknown>> = {};
|
private cache: Record<string, DerivedState<unknown>> = {};
|
||||||
|
|
||||||
constructor(protected storageServiceProvider: StorageServiceProvider) {}
|
constructor() {}
|
||||||
|
|
||||||
get<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
get<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||||
parentState$: Observable<TFrom>,
|
parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
dependencies: TDeps,
|
dependencies: TDeps,
|
||||||
): DerivedState<TTo> {
|
): DerivedState<TTo> {
|
||||||
// TODO: we probably want to support optional normal memory storage for browser
|
const cacheKey = deriveDefinition.buildCacheKey();
|
||||||
const [location, storageService] = this.storageServiceProvider.get("memory", {
|
|
||||||
browser: "memory-large-object",
|
|
||||||
});
|
|
||||||
const cacheKey = deriveDefinition.buildCacheKey(location);
|
|
||||||
const existingDerivedState = this.cache[cacheKey];
|
const existingDerivedState = this.cache[cacheKey];
|
||||||
if (existingDerivedState != null) {
|
if (existingDerivedState != null) {
|
||||||
// I have to cast out of the unknown generic but this should be safe if rules
|
// I have to cast out of the unknown generic but this should be safe if rules
|
||||||
@@ -34,10 +25,7 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider {
|
|||||||
return existingDerivedState as DefaultDerivedState<TFrom, TTo, TDeps>;
|
return existingDerivedState as DefaultDerivedState<TFrom, TTo, TDeps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies, [
|
const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies);
|
||||||
location,
|
|
||||||
storageService,
|
|
||||||
]);
|
|
||||||
this.cache[cacheKey] = newDerivedState;
|
this.cache[cacheKey] = newDerivedState;
|
||||||
return newDerivedState;
|
return newDerivedState;
|
||||||
}
|
}
|
||||||
@@ -46,13 +34,7 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider {
|
|||||||
parentState$: Observable<TFrom>,
|
parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
dependencies: TDeps,
|
dependencies: TDeps,
|
||||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
|
||||||
): DerivedState<TTo> {
|
): DerivedState<TTo> {
|
||||||
return new DefaultDerivedState<TFrom, TTo, TDeps>(
|
return new DefaultDerivedState<TFrom, TTo, TDeps>(parentState$, deriveDefinition, dependencies);
|
||||||
parentState$,
|
|
||||||
deriveDefinition,
|
|
||||||
storageLocation[1],
|
|
||||||
dependencies,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import { Subject, firstValueFrom } from "rxjs";
|
import { Subject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { awaitAsync, trackEmissions } from "../../../../spec";
|
import { awaitAsync, trackEmissions } from "../../../../spec";
|
||||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
|
||||||
import { DeriveDefinition } from "../derive-definition";
|
import { DeriveDefinition } from "../derive-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
|
||||||
@@ -29,7 +28,6 @@ const deriveDefinition = new DeriveDefinition<string, Date, { date: Date }>(
|
|||||||
|
|
||||||
describe("DefaultDerivedState", () => {
|
describe("DefaultDerivedState", () => {
|
||||||
let parentState$: Subject<string>;
|
let parentState$: Subject<string>;
|
||||||
let memoryStorage: FakeStorageService;
|
|
||||||
let sut: DefaultDerivedState<string, Date, { date: Date }>;
|
let sut: DefaultDerivedState<string, Date, { date: Date }>;
|
||||||
const deps = {
|
const deps = {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
@@ -38,8 +36,7 @@ describe("DefaultDerivedState", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
callCount = 0;
|
callCount = 0;
|
||||||
parentState$ = new Subject();
|
parentState$ = new Subject();
|
||||||
memoryStorage = new FakeStorageService();
|
sut = new DefaultDerivedState(parentState$, deriveDefinition, deps);
|
||||||
sut = new DefaultDerivedState(parentState$, deriveDefinition, memoryStorage, deps);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -66,71 +63,33 @@ describe("DefaultDerivedState", () => {
|
|||||||
expect(callCount).toBe(1);
|
expect(callCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should store the derived state in memory", async () => {
|
|
||||||
const dateString = "2020-01-01";
|
|
||||||
trackEmissions(sut.state$);
|
|
||||||
parentState$.next(dateString);
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
|
|
||||||
derivedValue(new Date(dateString)),
|
|
||||||
);
|
|
||||||
const calls = memoryStorage.mock.save.mock.calls;
|
|
||||||
expect(calls.length).toBe(1);
|
|
||||||
expect(calls[0][0]).toBe(deriveDefinition.storageKey);
|
|
||||||
expect(calls[0][1]).toEqual(derivedValue(new Date(dateString)));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("forceValue", () => {
|
describe("forceValue", () => {
|
||||||
const initialParentValue = "2020-01-01";
|
const initialParentValue = "2020-01-01";
|
||||||
const forced = new Date("2020-02-02");
|
const forced = new Date("2020-02-02");
|
||||||
let emissions: Date[];
|
let emissions: Date[];
|
||||||
|
|
||||||
describe("without observers", () => {
|
beforeEach(async () => {
|
||||||
beforeEach(async () => {
|
emissions = trackEmissions(sut.state$);
|
||||||
parentState$.next(initialParentValue);
|
parentState$.next(initialParentValue);
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
});
|
|
||||||
|
|
||||||
it("should store the forced value", async () => {
|
|
||||||
await sut.forceValue(forced);
|
|
||||||
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
|
|
||||||
derivedValue(forced),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with observers", () => {
|
it("should force the value", async () => {
|
||||||
beforeEach(async () => {
|
await sut.forceValue(forced);
|
||||||
emissions = trackEmissions(sut.state$);
|
expect(emissions).toEqual([new Date(initialParentValue), forced]);
|
||||||
parentState$.next(initialParentValue);
|
});
|
||||||
await awaitAsync();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should store the forced value", async () => {
|
it("should only force the value once", async () => {
|
||||||
await sut.forceValue(forced);
|
await sut.forceValue(forced);
|
||||||
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
|
|
||||||
derivedValue(forced),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should force the value", async () => {
|
parentState$.next(initialParentValue);
|
||||||
await sut.forceValue(forced);
|
await awaitAsync();
|
||||||
expect(emissions).toEqual([new Date(initialParentValue), forced]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should only force the value once", async () => {
|
expect(emissions).toEqual([
|
||||||
await sut.forceValue(forced);
|
new Date(initialParentValue),
|
||||||
|
forced,
|
||||||
parentState$.next(initialParentValue);
|
new Date(initialParentValue),
|
||||||
await awaitAsync();
|
]);
|
||||||
|
|
||||||
expect(emissions).toEqual([
|
|
||||||
new Date(initialParentValue),
|
|
||||||
forced,
|
|
||||||
new Date(initialParentValue),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,42 +107,6 @@ describe("DefaultDerivedState", () => {
|
|||||||
expect(parentState$.observed).toBe(false);
|
expect(parentState$.observed).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clear state after cleanup", async () => {
|
|
||||||
const subscription = sut.state$.subscribe();
|
|
||||||
parentState$.next(newDate);
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
|
|
||||||
derivedValue(new Date(newDate)),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscription.unsubscribe();
|
|
||||||
// Wait for cleanup
|
|
||||||
await awaitAsync(cleanupDelayMs * 2);
|
|
||||||
|
|
||||||
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not clear state after cleanup if clearOnCleanup is false", async () => {
|
|
||||||
deriveDefinition.options.clearOnCleanup = false;
|
|
||||||
|
|
||||||
const subscription = sut.state$.subscribe();
|
|
||||||
parentState$.next(newDate);
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
|
|
||||||
derivedValue(new Date(newDate)),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscription.unsubscribe();
|
|
||||||
// Wait for cleanup
|
|
||||||
await awaitAsync(cleanupDelayMs * 2);
|
|
||||||
|
|
||||||
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
|
|
||||||
derivedValue(new Date(newDate)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not cleanup if there are still subscribers", async () => {
|
it("should not cleanup if there are still subscribers", async () => {
|
||||||
const subscription1 = sut.state$.subscribe();
|
const subscription1 = sut.state$.subscribe();
|
||||||
const sub2Emissions: Date[] = [];
|
const sub2Emissions: Date[] = [];
|
||||||
@@ -260,7 +183,3 @@ describe("DefaultDerivedState", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function derivedValue<T>(value: T) {
|
|
||||||
return { derived: true, value };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Observable, ReplaySubject, Subject, concatMap, merge, share, timer } from "rxjs";
|
import { Observable, ReplaySubject, Subject, concatMap, merge, share, timer } from "rxjs";
|
||||||
|
|
||||||
import { DerivedStateDependencies } from "../../../types/state";
|
import { DerivedStateDependencies } from "../../../types/state";
|
||||||
import {
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "../../abstractions/storage.service";
|
|
||||||
import { DeriveDefinition } from "../derive-definition";
|
import { DeriveDefinition } from "../derive-definition";
|
||||||
import { DerivedState } from "../derived-state";
|
import { DerivedState } from "../derived-state";
|
||||||
|
|
||||||
@@ -22,7 +18,6 @@ export class DefaultDerivedState<TFrom, TTo, TDeps extends DerivedStateDependenc
|
|||||||
constructor(
|
constructor(
|
||||||
private parentState$: Observable<TFrom>,
|
private parentState$: Observable<TFrom>,
|
||||||
protected deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
protected deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
private memoryStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
private dependencies: TDeps,
|
private dependencies: TDeps,
|
||||||
) {
|
) {
|
||||||
this.storageKey = deriveDefinition.storageKey;
|
this.storageKey = deriveDefinition.storageKey;
|
||||||
@@ -34,7 +29,6 @@ export class DefaultDerivedState<TFrom, TTo, TDeps extends DerivedStateDependenc
|
|||||||
derivedStateOrPromise = await derivedStateOrPromise;
|
derivedStateOrPromise = await derivedStateOrPromise;
|
||||||
}
|
}
|
||||||
const derivedState = derivedStateOrPromise;
|
const derivedState = derivedStateOrPromise;
|
||||||
await this.storeValue(derivedState);
|
|
||||||
return derivedState;
|
return derivedState;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -44,26 +38,13 @@ export class DefaultDerivedState<TFrom, TTo, TDeps extends DerivedStateDependenc
|
|||||||
connector: () => {
|
connector: () => {
|
||||||
return new ReplaySubject<TTo>(1);
|
return new ReplaySubject<TTo>(1);
|
||||||
},
|
},
|
||||||
resetOnRefCountZero: () =>
|
resetOnRefCountZero: () => timer(this.deriveDefinition.cleanupDelayMs),
|
||||||
timer(this.deriveDefinition.cleanupDelayMs).pipe(
|
|
||||||
concatMap(async () => {
|
|
||||||
if (this.deriveDefinition.clearOnCleanup) {
|
|
||||||
await this.memoryStorage.remove(this.storageKey);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async forceValue(value: TTo) {
|
async forceValue(value: TTo) {
|
||||||
await this.storeValue(value);
|
|
||||||
this.forcedValueSubject.next(value);
|
this.forcedValueSubject.next(value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeValue(value: TTo) {
|
|
||||||
return this.memoryStorage.save(this.storageKey, { derived: true, value });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user