mirror of
https://github.com/bitwarden/browser
synced 2025-12-31 23:53:37 +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:
31
libs/storage-core/src/client-locations.ts
Normal file
31
libs/storage-core/src/client-locations.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { StorageLocation } from "./storage-location";
|
||||
|
||||
/**
|
||||
* *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;
|
||||
};
|
||||
7
libs/storage-core/src/html-storage-location.enum.ts
Normal file
7
libs/storage-core/src/html-storage-location.enum.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 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",
|
||||
}
|
||||
11
libs/storage-core/src/index.ts
Normal file
11
libs/storage-core/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./client-locations";
|
||||
export * from "./html-storage-location.enum";
|
||||
export * from "./memory-storage.service";
|
||||
export * from "./serialized-memory-storage.service";
|
||||
export * from "./storage-location";
|
||||
export * from "./storage-location.enum";
|
||||
export * from "./storage-options";
|
||||
export * from "./storage-service.provider";
|
||||
// Renamed to just "StorageService", to be removed when references are updated
|
||||
export { StorageService as AbstractStorageService } from "./storage.service";
|
||||
export * from "./storage.service";
|
||||
47
libs/storage-core/src/memory-storage.service.ts
Normal file
47
libs/storage-core/src/memory-storage.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { StorageService, StorageUpdate } from "./storage.service";
|
||||
|
||||
export class MemoryStorageService extends StorageService {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { SerializedMemoryStorageService } from "./serialized-memory-storage.service";
|
||||
|
||||
describe("SerializedMemoryStorageService", () => {
|
||||
let sut: SerializedMemoryStorageService;
|
||||
const key = "key";
|
||||
const value = { test: "value" };
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new SerializedMemoryStorageService();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
50
libs/storage-core/src/serialized-memory-storage.service.ts
Normal file
50
libs/storage-core/src/serialized-memory-storage.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { StorageService, ObservableStorageService, StorageUpdate } from "./storage.service";
|
||||
|
||||
export class SerializedMemoryStorageService
|
||||
extends StorageService
|
||||
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();
|
||||
}
|
||||
}
|
||||
8
libs/storage-core/src/storage-core.spec.ts
Normal file
8
libs/storage-core/src/storage-core.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as lib from "./index";
|
||||
|
||||
describe("storage-core", () => {
|
||||
// This test will fail until something is exported from index.ts
|
||||
it("should work", () => {
|
||||
expect(lib).toBeDefined();
|
||||
});
|
||||
});
|
||||
7
libs/storage-core/src/storage-location.enum.ts
Normal file
7
libs/storage-core/src/storage-location.enum.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum StorageLocationEnum {
|
||||
Both = "both",
|
||||
Disk = "disk",
|
||||
Memory = "memory",
|
||||
}
|
||||
12
libs/storage-core/src/storage-location.ts
Normal file
12
libs/storage-core/src/storage-location.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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";
|
||||
10
libs/storage-core/src/storage-options.ts
Normal file
10
libs/storage-core/src/storage-options.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { HtmlStorageLocation } from "./html-storage-location.enum";
|
||||
import { StorageLocationEnum as StorageLocation } from "./storage-location.enum";
|
||||
|
||||
export type StorageOptions = {
|
||||
storageLocation?: StorageLocation;
|
||||
useSecureStorage?: boolean;
|
||||
userId?: string;
|
||||
htmlStorageLocation?: HtmlStorageLocation;
|
||||
keySuffix?: string;
|
||||
};
|
||||
27
libs/storage-core/src/storage-service.provider.spec.ts
Normal file
27
libs/storage-core/src/storage-service.provider.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { StorageServiceProvider } from "./storage-service.provider";
|
||||
import { StorageService, ObservableStorageService } from "./storage.service";
|
||||
|
||||
describe("StorageServiceProvider", () => {
|
||||
const mockDiskStorage = mock<StorageService & ObservableStorageService>();
|
||||
const mockMemoryStorage = mock<StorageService & 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
39
libs/storage-core/src/storage-service.provider.ts
Normal file
39
libs/storage-core/src/storage-service.provider.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ClientLocations } from "./client-locations";
|
||||
import { StorageLocation } from "./storage-location";
|
||||
import { StorageService, ObservableStorageService } from "./storage.service";
|
||||
|
||||
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: StorageService & ObservableStorageService,
|
||||
protected readonly memoryStorageService: StorageService & 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: StorageService & ObservableStorageService] {
|
||||
switch (defaultLocation) {
|
||||
case "disk":
|
||||
return [defaultLocation, this.diskStorageService];
|
||||
case "memory":
|
||||
return [defaultLocation, this.memoryStorageService];
|
||||
default:
|
||||
throw new Error(`Unexpected location: ${defaultLocation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
libs/storage-core/src/storage.service.ts
Normal file
26
libs/storage-core/src/storage.service.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { StorageOptions } from "./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 StorageService {
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user