1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 18:33:50 +00:00

Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Victoria League
2024-04-23 09:52:28 -04:00
committed by GitHub
47 changed files with 627 additions and 656 deletions

View File

@@ -1046,7 +1046,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: DerivedStateProvider,
useClass: DefaultDerivedStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE],
deps: [StorageServiceProvider],
}),
safeProvider({
provide: StateProvider,

View File

@@ -249,11 +249,11 @@ export class FakeDerivedStateProvider implements DerivedStateProvider {
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: TDeps,
): DerivedState<TTo> {
let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState<TTo>;
let result = this.states.get(deriveDefinition.buildCacheKey("memory")) as DerivedState<TTo>;
if (result == null) {
result = new FakeDerivedState(parentState$, deriveDefinition, dependencies);
this.states.set(deriveDefinition.buildCacheKey(), result);
this.states.set(deriveDefinition.buildCacheKey("memory"), result);
}
return result;
}

View File

@@ -4,3 +4,4 @@ export * from "./matchers";
export * from "./fake-state-provider";
export * from "./fake-state";
export * from "./fake-account-service";
export * from "./fake-storage.service";

View File

@@ -0,0 +1,85 @@
import { Lazy } from "./lazy";
describe("Lazy", () => {
afterEach(() => {
jest.resetAllMocks();
});
describe("async", () => {
let factory: jest.Mock<Promise<number>>;
let lazy: Lazy<Promise<number>>;
beforeEach(() => {
factory = jest.fn();
lazy = new Lazy(factory);
});
describe("get", () => {
it("should call the factory once", async () => {
await lazy.get();
await lazy.get();
expect(factory).toHaveBeenCalledTimes(1);
});
it("should return the value from the factory", async () => {
factory.mockResolvedValue(42);
const value = await lazy.get();
expect(value).toBe(42);
});
});
describe("factory throws", () => {
it("should throw the error", async () => {
factory.mockRejectedValue(new Error("factory error"));
await expect(lazy.get()).rejects.toThrow("factory error");
});
});
describe("factory returns undefined", () => {
it("should return undefined", async () => {
factory.mockResolvedValue(undefined);
const value = await lazy.get();
expect(value).toBeUndefined();
});
});
describe("factory returns null", () => {
it("should return null", async () => {
factory.mockResolvedValue(null);
const value = await lazy.get();
expect(value).toBeNull();
});
});
});
describe("sync", () => {
const syncFactory = jest.fn();
let lazy: Lazy<number>;
beforeEach(() => {
syncFactory.mockReturnValue(42);
lazy = new Lazy<number>(syncFactory);
});
it("should return the value from the factory", () => {
const value = lazy.get();
expect(value).toBe(42);
});
it("should call the factory once", () => {
lazy.get();
lazy.get();
expect(syncFactory).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,20 @@
export class Lazy<T> {
private _value: T | undefined = undefined;
private _isCreated = false;
constructor(private readonly factory: () => T) {}
/**
* Resolves the factory and returns the result. Guaranteed to resolve the value only once.
*
* @returns The value produced by your factory.
*/
get(): T {
if (!this._isCreated) {
this._value = this.factory();
this._isCreated = true;
}
return this._value as T;
}
}

View File

@@ -171,8 +171,8 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies
return this.options.clearOnCleanup ?? true;
}
buildCacheKey(): string {
return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}`;
buildCacheKey(location: string): string {
return `derived_${location}_${this.stateDefinition.name}_${this.uniqueDerivationName}`;
}
/**

View File

@@ -5,6 +5,7 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DerivedStateProvider } from "../derived-state.provider";
@@ -14,14 +15,18 @@ import { DefaultDerivedState } from "./default-derived-state";
export class DefaultDerivedStateProvider implements DerivedStateProvider {
private cache: Record<string, DerivedState<unknown>> = {};
constructor(protected memoryStorage: AbstractStorageService & ObservableStorageService) {}
constructor(protected storageServiceProvider: StorageServiceProvider) {}
get<TFrom, TTo, TDeps extends DerivedStateDependencies>(
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: TDeps,
): DerivedState<TTo> {
const cacheKey = deriveDefinition.buildCacheKey();
// TODO: we probably want to support optional normal memory storage for browser
const [location, storageService] = this.storageServiceProvider.get("memory", {
browser: "memory-large-object",
});
const cacheKey = deriveDefinition.buildCacheKey(location);
const existingDerivedState = this.cache[cacheKey];
if (existingDerivedState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
@@ -29,7 +34,10 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider {
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;
return newDerivedState;
}
@@ -38,11 +46,12 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider {
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: TDeps,
storageLocation: [string, AbstractStorageService & ObservableStorageService],
): DerivedState<TTo> {
return new DefaultDerivedState<TFrom, TTo, TDeps>(
parentState$,
deriveDefinition,
this.memoryStorage,
storageLocation[1],
dependencies,
);
}

View File

@@ -72,12 +72,12 @@ describe("DefaultDerivedState", () => {
parentState$.next(dateString);
await awaitAsync();
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(
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.buildCacheKey());
expect(calls[0][0]).toBe(deriveDefinition.storageKey);
expect(calls[0][1]).toEqual(derivedValue(new Date(dateString)));
});
@@ -94,7 +94,7 @@ describe("DefaultDerivedState", () => {
it("should store the forced value", async () => {
await sut.forceValue(forced);
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
derivedValue(forced),
);
});
@@ -109,7 +109,7 @@ describe("DefaultDerivedState", () => {
it("should store the forced value", async () => {
await sut.forceValue(forced);
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
derivedValue(forced),
);
});
@@ -153,7 +153,7 @@ describe("DefaultDerivedState", () => {
parentState$.next(newDate);
await awaitAsync();
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
derivedValue(new Date(newDate)),
);
@@ -161,7 +161,7 @@ describe("DefaultDerivedState", () => {
// Wait for cleanup
await awaitAsync(cleanupDelayMs * 2);
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toBeUndefined();
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toBeUndefined();
});
it("should not clear state after cleanup if clearOnCleanup is false", async () => {
@@ -171,7 +171,7 @@ describe("DefaultDerivedState", () => {
parentState$.next(newDate);
await awaitAsync();
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
derivedValue(new Date(newDate)),
);
@@ -179,7 +179,7 @@ describe("DefaultDerivedState", () => {
// Wait for cleanup
await awaitAsync(cleanupDelayMs * 2);
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(
expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual(
derivedValue(new Date(newDate)),
);
});

View File

@@ -24,8 +24,10 @@ export type ClientLocations = {
web: StorageLocation | "disk-local";
/**
* Overriding storage location for browser clients.
*
* "memory-large-object" is used to store non-countable objects in memory. This exists due to limited persistent memory available to browser extensions.
*/
//browser: StorageLocation;
browser: StorageLocation | "memory-large-object";
/**
* Overriding storage location for desktop clients.
*/

View File

@@ -116,7 +116,9 @@ export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "dis
export const SEND_DISK = new StateDefinition("encryptedSend", "disk", {
web: "memory",
});
export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory");
export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory", {
browser: "memory-large-object",
});
// Vault
@@ -133,10 +135,16 @@ export const VAULT_ONBOARDING = new StateDefinition("vaultOnboarding", "disk", {
export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", {
web: "disk-local",
});
export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory");
export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory");
export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory", {
browser: "memory-large-object",
});
export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory", {
browser: "memory-large-object",
});
export const CIPHERS_DISK = new StateDefinition("ciphers", "disk", { web: "memory" });
export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", {
web: "disk-local",
});
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory");
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", {
browser: "memory-large-object",
});

View File

@@ -36,7 +36,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
if (!(await this.shouldUpdate(cipherId, organizationId))) {
if (!(await this.shouldUpdate(cipherId, organizationId, eventType))) {
return;
}
@@ -64,6 +64,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
private async shouldUpdate(
cipherId: string = null,
organizationId: string = null,
eventType: EventType = null,
): Promise<boolean> {
const orgIds$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []),
@@ -85,6 +86,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
return false;
}
// Individual vault export doesn't need cipher id or organization id.
if (eventType == EventType.User_ClientExportedVault) {
return true;
}
// If the cipher is null there must be an organization id provided
if (cipher == null && organizationId == null) {
return false;

View File

@@ -47,3 +47,8 @@ $card-icons-base: "../../src/billing/images/cards/";
@import "bootstrap/scss/_print";
@import "multi-select/scss/bw.theme.scss";
// Workaround for https://bitwarden.atlassian.net/browse/CL-110
#storybook-docs pre.prismjs {
color: white;
}