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
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -90,6 +90,7 @@ libs/common/src/platform @bitwarden/team-platform-dev
|
|||||||
libs/common/spec @bitwarden/team-platform-dev
|
libs/common/spec @bitwarden/team-platform-dev
|
||||||
libs/common/src/state-migrations @bitwarden/team-platform-dev
|
libs/common/src/state-migrations @bitwarden/team-platform-dev
|
||||||
libs/platform @bitwarden/team-platform-dev
|
libs/platform @bitwarden/team-platform-dev
|
||||||
|
libs/storage-core @bitwarden/team-platform-dev
|
||||||
# Web utils used across app and connectors
|
# Web utils used across app and connectors
|
||||||
apps/web/src/utils/ @bitwarden/team-platform-dev
|
apps/web/src/utils/ @bitwarden/team-platform-dev
|
||||||
# Web core and shared files
|
# Web core and shared files
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
import { Observable } from "rxjs";
|
export {
|
||||||
|
StorageUpdateType,
|
||||||
import { StorageOptions } from "../models/domain/storage-options";
|
StorageUpdate,
|
||||||
|
ObservableStorageService,
|
||||||
export type StorageUpdateType = "save" | "remove";
|
AbstractStorageService,
|
||||||
export type StorageUpdate = {
|
} from "@bitwarden/storage-core";
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
// FIXME: update to use a const object instead of a typescript enum
|
export { HtmlStorageLocation } from "@bitwarden/storage-core";
|
||||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
|
||||||
export enum HtmlStorageLocation {
|
|
||||||
Local = "local",
|
|
||||||
Memory = "memory",
|
|
||||||
Session = "session",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
// FIXME: update to use a const object instead of a typescript enum
|
export { StorageLocationEnum as StorageLocation } from "@bitwarden/storage-core";
|
||||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
|
||||||
export enum StorageLocation {
|
|
||||||
Both = "both",
|
|
||||||
Disk = "disk",
|
|
||||||
Memory = "memory",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1 @@
|
|||||||
import { HtmlStorageLocation, StorageLocation } from "../../enums";
|
export type { StorageOptions } from "@bitwarden/storage-core";
|
||||||
|
|
||||||
export type StorageOptions = {
|
|
||||||
storageLocation?: StorageLocation;
|
|
||||||
useSecureStorage?: boolean;
|
|
||||||
userId?: string;
|
|
||||||
htmlStorageLocation?: HtmlStorageLocation;
|
|
||||||
keySuffix?: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,47 +1 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
export { MemoryStorageService } from "@bitwarden/storage-core";
|
||||||
// @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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,39 +1,2 @@
|
|||||||
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
|
export { StorageServiceProvider } from "@bitwarden/storage-core";
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
export type { PossibleLocation } from "@bitwarden/storage-core";
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,45 +1,7 @@
|
|||||||
/**
|
import { StorageLocation, ClientLocations } from "@bitwarden/storage-core";
|
||||||
* 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";
|
|
||||||
|
|
||||||
/**
|
// To be removed once references are updated to point to @bitwarden/storage-core
|
||||||
* *Note*: The property names of this object should match exactly with the string values of the {@link ClientType} enum
|
export { StorageLocation, ClientLocations };
|
||||||
*/
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the base location and instruction of where this state is expected to be located.
|
* Defines the base location and instruction of where this state is expected to be located.
|
||||||
|
|||||||
@@ -1,54 +1 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
export { SerializedMemoryStorageService as MemoryStorageService } from "@bitwarden/storage-core";
|
||||||
// @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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
5
libs/storage-core/README.md
Normal file
5
libs/storage-core/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# storage-core
|
||||||
|
|
||||||
|
Owned by: platform
|
||||||
|
|
||||||
|
Abstractions over storage APIs
|
||||||
3
libs/storage-core/eslint.config.mjs
Normal file
3
libs/storage-core/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import baseConfig from "../../eslint.config.mjs";
|
||||||
|
|
||||||
|
export default [...baseConfig];
|
||||||
10
libs/storage-core/jest.config.js
Normal file
10
libs/storage-core/jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
displayName: "storage-core",
|
||||||
|
preset: "../../jest.preset.js",
|
||||||
|
testEnvironment: "node",
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ["ts", "js", "html"],
|
||||||
|
coverageDirectory: "../../coverage/libs/storage-core",
|
||||||
|
};
|
||||||
11
libs/storage-core/package.json
Normal file
11
libs/storage-core/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@bitwarden/storage-core",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Abstractions over storage APIs",
|
||||||
|
"private": true,
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"author": "platform"
|
||||||
|
}
|
||||||
33
libs/storage-core/project.json
Normal file
33
libs/storage-core/project.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "storage-core",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/storage-core/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nx/js:tsc",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/libs/storage-core",
|
||||||
|
"main": "libs/storage-core/src/index.ts",
|
||||||
|
"tsConfig": "libs/storage-core/tsconfig.lib.json",
|
||||||
|
"assets": ["libs/storage-core/*.md"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint",
|
||||||
|
"outputs": ["{options.outputFile}"],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/storage-core/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/storage-core/jest.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { MemoryStorageService } from "./memory-storage.service";
|
import { SerializedMemoryStorageService } from "./serialized-memory-storage.service";
|
||||||
|
|
||||||
describe("MemoryStorageService", () => {
|
describe("SerializedMemoryStorageService", () => {
|
||||||
let sut: MemoryStorageService;
|
let sut: SerializedMemoryStorageService;
|
||||||
const key = "key";
|
const key = "key";
|
||||||
const value = { test: "value" };
|
const value = { test: "value" };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sut = new MemoryStorageService();
|
sut = new SerializedMemoryStorageService();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
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;
|
||||||
|
};
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
|
|
||||||
|
|
||||||
import { StorageServiceProvider } from "./storage-service.provider";
|
import { StorageServiceProvider } from "./storage-service.provider";
|
||||||
|
import { StorageService, ObservableStorageService } from "./storage.service";
|
||||||
|
|
||||||
describe("StorageServiceProvider", () => {
|
describe("StorageServiceProvider", () => {
|
||||||
const mockDiskStorage = mock<AbstractStorageService & ObservableStorageService>();
|
const mockDiskStorage = mock<StorageService & ObservableStorageService>();
|
||||||
const mockMemoryStorage = mock<AbstractStorageService & ObservableStorageService>();
|
const mockMemoryStorage = mock<StorageService & ObservableStorageService>();
|
||||||
|
|
||||||
const sut = new StorageServiceProvider(mockDiskStorage, mockMemoryStorage);
|
const sut = new StorageServiceProvider(mockDiskStorage, 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>;
|
||||||
|
}
|
||||||
13
libs/storage-core/tsconfig.json
Normal file
13
libs/storage-core/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10
libs/storage-core/tsconfig.lib.json
Normal file
10
libs/storage-core/tsconfig.lib.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"declaration": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
10
libs/storage-core/tsconfig.spec.json
Normal file
10
libs/storage-core/tsconfig.spec.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node10",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||||
|
}
|
||||||
2666
package-lock.json
generated
2666
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@
|
|||||||
"@bitwarden/platform": ["./libs/platform/src"],
|
"@bitwarden/platform": ["./libs/platform/src"],
|
||||||
"@bitwarden/platform/*": ["./libs/platform/src/*"],
|
"@bitwarden/platform/*": ["./libs/platform/src/*"],
|
||||||
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
|
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
|
||||||
|
"@bitwarden/storage-core": ["libs/storage-core/src/index.ts"],
|
||||||
"@bitwarden/ui-common": ["./libs/ui/common/src"],
|
"@bitwarden/ui-common": ["./libs/ui/common/src"],
|
||||||
"@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"],
|
"@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"],
|
||||||
"@bitwarden/vault": ["./libs/vault/src"],
|
"@bitwarden/vault": ["./libs/vault/src"],
|
||||||
|
|||||||
Reference in New Issue
Block a user