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:
@@ -1046,7 +1046,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: DerivedStateProvider,
|
||||
useClass: DefaultDerivedStateProvider,
|
||||
deps: [OBSERVABLE_MEMORY_STORAGE],
|
||||
deps: [StorageServiceProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateProvider,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
85
libs/common/src/platform/misc/lazy.spec.ts
Normal file
85
libs/common/src/platform/misc/lazy.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
libs/common/src/platform/misc/lazy.ts
Normal file
20
libs/common/src/platform/misc/lazy.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user