mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
refactor(storage-core): move storage files out of @bitwarden/common (#15076)
* refactor(platform): generate @bitwarden/storage-core boilerplate * refactor(storage-core): move storage files out of @bitwarden/common * chore(naming): rename AbstractStorageService to StorageService
This commit is contained in:
@@ -1,26 +1,6 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
|
||||
export type StorageUpdateType = "save" | "remove";
|
||||
export type StorageUpdate = {
|
||||
key: string;
|
||||
updateType: StorageUpdateType;
|
||||
};
|
||||
|
||||
export interface ObservableStorageService {
|
||||
/**
|
||||
* Provides an {@link Observable} that represents a stream of updates that
|
||||
* have happened in this storage service or in the storage this service provides
|
||||
* an interface to.
|
||||
*/
|
||||
get updates$(): Observable<StorageUpdate>;
|
||||
}
|
||||
|
||||
export abstract class AbstractStorageService {
|
||||
abstract get valuesRequireDeserialization(): boolean;
|
||||
abstract get<T>(key: string, options?: StorageOptions): Promise<T>;
|
||||
abstract has(key: string, options?: StorageOptions): Promise<boolean>;
|
||||
abstract save<T>(key: string, obj: T, options?: StorageOptions): Promise<void>;
|
||||
abstract remove(key: string, options?: StorageOptions): Promise<void>;
|
||||
}
|
||||
export {
|
||||
StorageUpdateType,
|
||||
StorageUpdate,
|
||||
ObservableStorageService,
|
||||
AbstractStorageService,
|
||||
} from "@bitwarden/storage-core";
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum HtmlStorageLocation {
|
||||
Local = "local",
|
||||
Memory = "memory",
|
||||
Session = "session",
|
||||
}
|
||||
export { HtmlStorageLocation } from "@bitwarden/storage-core";
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum StorageLocation {
|
||||
Both = "both",
|
||||
Disk = "disk",
|
||||
Memory = "memory",
|
||||
}
|
||||
export { StorageLocationEnum as StorageLocation } from "@bitwarden/storage-core";
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
import { HtmlStorageLocation, StorageLocation } from "../../enums";
|
||||
|
||||
export type StorageOptions = {
|
||||
storageLocation?: StorageLocation;
|
||||
useSecureStorage?: boolean;
|
||||
userId?: string;
|
||||
htmlStorageLocation?: HtmlStorageLocation;
|
||||
keySuffix?: string;
|
||||
};
|
||||
export type { StorageOptions } from "@bitwarden/storage-core";
|
||||
|
||||
@@ -1,47 +1 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { AbstractStorageService, StorageUpdate } from "../abstractions/storage.service";
|
||||
|
||||
export class MemoryStorageService extends AbstractStorageService {
|
||||
protected store = new Map<string, unknown>();
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
|
||||
get valuesRequireDeserialization(): boolean {
|
||||
return false;
|
||||
}
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
if (this.store.has(key)) {
|
||||
const obj = this.store.get(key);
|
||||
return Promise.resolve(obj as T);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return (await this.get(key)) != null;
|
||||
}
|
||||
|
||||
save<T>(key: string, obj: T): Promise<void> {
|
||||
if (obj == null) {
|
||||
return this.remove(key);
|
||||
}
|
||||
// TODO: Remove once foreground/background contexts are separated in browser
|
||||
// Needed to ensure ownership of all memory by the context running the storage service
|
||||
const toStore = structuredClone(obj);
|
||||
this.store.set(key, toStore);
|
||||
this.updatesSubject.next({ key, updateType: "save" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
remove(key: string): Promise<void> {
|
||||
this.store.delete(key);
|
||||
this.updatesSubject.next({ key, updateType: "remove" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
export { MemoryStorageService } from "@bitwarden/storage-core";
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
|
||||
|
||||
import { StorageServiceProvider } from "./storage-service.provider";
|
||||
|
||||
describe("StorageServiceProvider", () => {
|
||||
const mockDiskStorage = mock<AbstractStorageService & ObservableStorageService>();
|
||||
const mockMemoryStorage = mock<AbstractStorageService & ObservableStorageService>();
|
||||
|
||||
const sut = new StorageServiceProvider(mockDiskStorage, mockMemoryStorage);
|
||||
|
||||
describe("get", () => {
|
||||
it("gets disk service when default location is disk", () => {
|
||||
const [computedLocation, computedService] = sut.get("disk", {});
|
||||
|
||||
expect(computedLocation).toBe("disk");
|
||||
expect(computedService).toStrictEqual(mockDiskStorage);
|
||||
});
|
||||
|
||||
it("gets memory service when default location is memory", () => {
|
||||
const [computedLocation, computedService] = sut.get("memory", {});
|
||||
|
||||
expect(computedLocation).toBe("memory");
|
||||
expect(computedService).toStrictEqual(mockMemoryStorage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +1,2 @@
|
||||
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import { ClientLocations, StorageLocation } from "../state/state-definition";
|
||||
|
||||
export type PossibleLocation = StorageLocation | ClientLocations[keyof ClientLocations];
|
||||
|
||||
/**
|
||||
* A provider for getting client specific computed storage locations and services.
|
||||
*/
|
||||
export class StorageServiceProvider {
|
||||
constructor(
|
||||
protected readonly diskStorageService: AbstractStorageService & ObservableStorageService,
|
||||
protected readonly memoryStorageService: AbstractStorageService & ObservableStorageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Computes the location and corresponding service for a given client.
|
||||
*
|
||||
* **NOTE** The default implementation does not respect client overrides and if clients
|
||||
* have special overrides they are responsible for implementing this service.
|
||||
* @param defaultLocation The default location to use if no client specific override is preferred.
|
||||
* @param overrides Client specific overrides
|
||||
* @returns The computed storage location and corresponding storage service to use to get/store state.
|
||||
* @throws If there is no configured storage service for the given inputs.
|
||||
*/
|
||||
get(
|
||||
defaultLocation: PossibleLocation,
|
||||
overrides: Partial<ClientLocations>,
|
||||
): [location: PossibleLocation, service: AbstractStorageService & ObservableStorageService] {
|
||||
switch (defaultLocation) {
|
||||
case "disk":
|
||||
return [defaultLocation, this.diskStorageService];
|
||||
case "memory":
|
||||
return [defaultLocation, this.memoryStorageService];
|
||||
default:
|
||||
throw new Error(`Unexpected location: ${defaultLocation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||
export type { PossibleLocation } from "@bitwarden/storage-core";
|
||||
|
||||
@@ -1,45 +1,7 @@
|
||||
/**
|
||||
* Default storage location options.
|
||||
*
|
||||
* `disk` generally means state that is accessible between restarts of the application,
|
||||
* with the exception of the web client. In web this means `sessionStorage`. The data
|
||||
* persists through refreshes of the page but not available once that tab is closed or
|
||||
* from any other tabs.
|
||||
*
|
||||
* `memory` means that the information stored there goes away during application
|
||||
* restarts.
|
||||
*/
|
||||
export type StorageLocation = "disk" | "memory";
|
||||
import { StorageLocation, ClientLocations } from "@bitwarden/storage-core";
|
||||
|
||||
/**
|
||||
* *Note*: The property names of this object should match exactly with the string values of the {@link ClientType} enum
|
||||
*/
|
||||
export type ClientLocations = {
|
||||
/**
|
||||
* Overriding storage location for the web client.
|
||||
*
|
||||
* Includes an extra storage location to store data in `localStorage`
|
||||
* that is available from different tabs and after a tab has closed.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* `"disk-backup-local-storage"` is used to store object in both disk and in `localStorage`. Data is stored in both locations but is only retrieved
|
||||
* from `localStorage` when a null-ish value is retrieved from disk first.
|
||||
*/
|
||||
browser: StorageLocation | "memory-large-object" | "disk-backup-local-storage";
|
||||
/**
|
||||
* Overriding storage location for desktop clients.
|
||||
*/
|
||||
//desktop: StorageLocation;
|
||||
/**
|
||||
* Overriding storage location for CLI clients.
|
||||
*/
|
||||
//cli: StorageLocation;
|
||||
};
|
||||
// To be removed once references are updated to point to @bitwarden/storage-core
|
||||
export { StorageLocation, ClientLocations };
|
||||
|
||||
/**
|
||||
* Defines the base location and instruction of where this state is expected to be located.
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { MemoryStorageService } from "./memory-storage.service";
|
||||
|
||||
describe("MemoryStorageService", () => {
|
||||
let sut: MemoryStorageService;
|
||||
const key = "key";
|
||||
const value = { test: "value" };
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new MemoryStorageService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("should return null if the key does not exist", async () => {
|
||||
const result = await sut.get(key);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the value if the key exists", async () => {
|
||||
await sut.save(key, value);
|
||||
const result = await sut.get(key);
|
||||
expect(result).toEqual(value);
|
||||
});
|
||||
|
||||
it("should json parse stored values", async () => {
|
||||
sut["store"][key] = JSON.stringify({ test: "value" });
|
||||
const result = await sut.get(key);
|
||||
|
||||
expect(result).toEqual({ test: "value" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
it("should store the value as json string", async () => {
|
||||
const value = { test: "value" };
|
||||
await sut.save(key, value);
|
||||
|
||||
expect(sut["store"][key]).toEqual(JSON.stringify(value));
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should remove a value from store", async () => {
|
||||
await sut.save(key, value);
|
||||
await sut.remove(key);
|
||||
|
||||
expect(Object.keys(sut["store"])).not.toContain(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,54 +1 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
StorageUpdate,
|
||||
} from "../../abstractions/storage.service";
|
||||
|
||||
export class MemoryStorageService
|
||||
extends AbstractStorageService
|
||||
implements ObservableStorageService
|
||||
{
|
||||
protected store: Record<string, string> = {};
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
|
||||
get valuesRequireDeserialization(): boolean {
|
||||
return true;
|
||||
}
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
const json = this.store[key];
|
||||
if (json) {
|
||||
const obj = JSON.parse(json as string);
|
||||
return Promise.resolve(obj as T);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return (await this.get(key)) != null;
|
||||
}
|
||||
|
||||
save<T>(key: string, obj: T): Promise<void> {
|
||||
if (obj == null) {
|
||||
return this.remove(key);
|
||||
}
|
||||
// TODO: Remove once foreground/background contexts are separated in browser
|
||||
// Needed to ensure ownership of all memory by the context running the storage service
|
||||
this.store[key] = JSON.stringify(obj);
|
||||
this.updatesSubject.next({ key, updateType: "save" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
remove(key: string): Promise<void> {
|
||||
delete this.store[key];
|
||||
this.updatesSubject.next({ key, updateType: "remove" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
export { SerializedMemoryStorageService as MemoryStorageService } from "@bitwarden/storage-core";
|
||||
|
||||
Reference in New Issue
Block a user