1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00

Add State Provider Framework (#6640)

* Add StateDefinition

Add a class for encapsulation information about state
this will often be for a domain but creations of this will
exist outside of a specific domain, hence just the name State.

* Add KeyDefinition

This adds a type that extends state definition into another sub-key
and forces creators to define the data that will be stored and how
to read the data that they expect to be stored.

* Add key-builders helper functions

Adds to function to help building keys for both keys scoped
to a specific user and for keys scoped to global storage.

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Add updates$ stream to existing storageServices

Original commit by Matt: 823d9546fe
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Add fromChromeEvent helper

Create a helper that creats an Observable from a chrome event
and removes the listener when the subscription is completed.

* Implement `updates$` property for chrome storage

Use fromChromeEvent to create an observable from chrome
event and map that into our expected shape.

* Add GlobalState Abstractions

* Add UserState Abstractions

* Add Default Implementations of User/Global state

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Add Barrel File for state

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Fix ChromeStorageServices

* Rework fromChromeEvent

Rework fromChromeEvent so we have to lie to TS less and
remove unneeded generics. I did this by caring less about
the function and more about the parameters only.

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Fix UserStateProvider Test

* Add Inner Mock & Assert Calls

* Update Tests to use new keys

Use different key format

* Prefer returns over mutations in update

* Update Tests

* Address PR Feedback

* Be stricter with userId parameter

* Add Better Way To Determine if it was a remove

* Fix Web & Browser Storage Services

* Fix Desktop & CLI Storage Services

* Fix Test Storage Service

* Use createKey Helper

* Prefer implement to extending

* Determine storage location in providers

* Export default providers publicly

* Fix user state tests

* Name tests

* Fix CLI

* Prefer Implement In Chrome Storage

* Remove Secure Storage Option

Also throw an exception for subscribes to the secure storage observable.

* Update apps/browser/src/platform/browser/from-chrome-event.ts

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Enforce state module barrel file

* Fix Linting Error

* Allow state module import from other modules

* Globally Unregister fromChromeEvent Listeners

Changed fromChromeEvent to add its listeners through the BrowserApi, so that
they will be unregistered when safari closes.

* Test default global state

* Use Proper Casing in Parameter

* Address Feedback

* Update libs/common/src/platform/state/key-definition.ts

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Add `buildCacheKey` Method

* Fix lint errors

* Add Comment

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Use Generic in callback parameter

* Refactor Out DerivedStateDefinition

* Persist Listener Return Type

* Add Ticket Link

---------

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
Justin Baur
2023-11-09 17:06:42 -05:00
committed by GitHub
parent 801141f90e
commit e1b5b83723
36 changed files with 1352 additions and 68 deletions

View File

@@ -193,6 +193,9 @@ export class BrowserApi {
}
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
// and test that it doesn't break.
// eslint-disable-next-line no-restricted-syntax
return chrome.windows.onCreated.addListener(callback);
}
@@ -220,8 +223,10 @@ export class BrowserApi {
// Keep track of all the events registered in a Safari popup so we can remove
// them when the popup gets unloaded, otherwise we cause a memory leak
private static registeredMessageListeners: any[] = [];
private static registeredStorageChangeListeners: any[] = [];
private static trackedChromeEventListeners: [
event: chrome.events.Event<(...args: unknown[]) => unknown>,
callback: (...args: unknown[]) => unknown
][] = [];
static messageListener(
name: string,
@@ -231,13 +236,7 @@ export class BrowserApi {
sendResponse: any
) => boolean | void
) {
// eslint-disable-next-line no-restricted-syntax
chrome.runtime.onMessage.addListener(callback);
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
BrowserApi.registeredMessageListeners.push(callback);
BrowserApi.setupUnloadListeners();
}
BrowserApi.addListener(chrome.runtime.onMessage, callback);
}
static messageListener$() {
@@ -246,44 +245,67 @@ export class BrowserApi {
subscriber.next(message);
};
BrowserApi.messageListener("message", handler);
BrowserApi.addListener(chrome.runtime.onMessage, handler);
return () => {
chrome.runtime.onMessage.removeListener(handler);
if (BrowserApi.isSafariApi) {
const index = BrowserApi.registeredMessageListeners.indexOf(handler);
if (index !== -1) {
BrowserApi.registeredMessageListeners.splice(index, 1);
}
}
};
return () => BrowserApi.removeListener(chrome.runtime.onMessage, handler);
});
}
static storageChangeListener(
callback: Parameters<typeof chrome.storage.onChanged.addListener>[0]
) {
// eslint-disable-next-line no-restricted-syntax
chrome.storage.onChanged.addListener(callback);
BrowserApi.addListener(chrome.storage.onChanged, callback);
}
/**
* Adds a callback to the given chrome event in a cross-browser platform manner.
*
* **Important:** All event listeners in the browser extension popup context must
* use this instead of the native APIs to handle unsubscribing from Safari properly.
*
* @param event - The event in which to add the listener to.
* @param callback - The callback you want registered onto the event.
*/
static addListener<T extends (...args: readonly unknown[]) => unknown>(
event: chrome.events.Event<T>,
callback: T
) {
event.addListener(callback);
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
BrowserApi.registeredStorageChangeListeners.push(callback);
BrowserApi.trackedChromeEventListeners.push([event, callback]);
BrowserApi.setupUnloadListeners();
}
}
/**
* Removes a callback from the given chrome event in a cross-browser platform manner.
* @param event - The event in which to remove the listener from.
* @param callback - The callback you want removed from the event.
*/
static removeListener<T extends (...args: readonly unknown[]) => unknown>(
event: chrome.events.Event<T>,
callback: T
) {
event.removeListener(callback);
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
const index = BrowserApi.trackedChromeEventListeners.findIndex(([_event, eventListener]) => {
return eventListener == callback;
});
if (index !== -1) {
BrowserApi.trackedChromeEventListeners.splice(index, 1);
}
}
}
// Setup the event to destroy all the listeners when the popup gets unloaded in Safari, otherwise we get a memory leak
private static setupUnloadListeners() {
// The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well
// 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one
window.onpagehide = () => {
for (const callback of BrowserApi.registeredMessageListeners) {
chrome.runtime.onMessage.removeListener(callback);
}
for (const callback of BrowserApi.registeredStorageChangeListeners) {
chrome.storage.onChanged.removeListener(callback);
for (const [event, callback] of BrowserApi.trackedChromeEventListeners) {
event.removeListener(callback);
}
};
}

View File

@@ -0,0 +1,103 @@
import { fromChromeEvent } from "./from-chrome-event";
describe("fromChromeEvent", () => {
class FakeEvent implements chrome.events.Event<(arg1: string, arg2: number) => void> {
listenerWasAdded: boolean;
listenerWasRemoved: boolean;
activeListeners: ((arg1: string, arg2: number) => void)[] = [];
addListener(callback: (arg1: string, arg2: number) => void): void {
this.listenerWasAdded = true;
this.activeListeners.push(callback);
}
getRules(callback: (rules: chrome.events.Rule[]) => void): void;
getRules(ruleIdentifiers: string[], callback: (rules: chrome.events.Rule[]) => void): void;
getRules(ruleIdentifiers: unknown, callback?: unknown): void {
throw new Error("Method not implemented.");
}
hasListener(callback: (arg1: string, arg2: number) => void): boolean {
throw new Error("Method not implemented.");
}
removeRules(ruleIdentifiers?: string[], callback?: () => void): void;
removeRules(callback?: () => void): void;
removeRules(ruleIdentifiers?: unknown, callback?: unknown): void {
throw new Error("Method not implemented.");
}
addRules(rules: chrome.events.Rule[], callback?: (rules: chrome.events.Rule[]) => void): void {
throw new Error("Method not implemented.");
}
removeListener(callback: (arg1: string, arg2: number) => void): void {
const index = this.activeListeners.findIndex((c) => c == callback);
if (index === -1) {
throw new Error("No registered callback.");
}
this.listenerWasRemoved = true;
this.activeListeners.splice(index, 1);
}
hasListeners(): boolean {
throw new Error("Method not implemented.");
}
fireEvent(arg1: string, arg2: number) {
this.activeListeners.forEach((listener) => {
listener(arg1, arg2);
});
}
}
let event: FakeEvent;
beforeEach(() => {
event = new FakeEvent();
});
it("should never call addListener when never subscribed to", () => {
fromChromeEvent(event);
expect(event.listenerWasAdded).toBeFalsy();
});
it("should add a listener when subscribed to.", () => {
const eventObservable = fromChromeEvent(event);
eventObservable.subscribe();
expect(event.listenerWasAdded).toBeTruthy();
expect(event.activeListeners).toHaveLength(1);
});
it("should call remove listener when the created subscription is unsubscribed", () => {
const eventObservable = fromChromeEvent(event);
const subscription = eventObservable.subscribe();
subscription.unsubscribe();
expect(event.listenerWasAdded).toBeTruthy();
expect(event.listenerWasRemoved).toBeTruthy();
expect(event.activeListeners).toHaveLength(0);
});
it("should fire each callback given to subscribe", () => {
const eventObservable = fromChromeEvent(event);
let subscription1Called = false;
let subscription2Called = false;
const subscription1 = eventObservable.subscribe(([arg1, arg2]) => {
expect(arg1).toBe("Hi!");
expect(arg2).toBe(2);
subscription1Called = true;
});
const subscription2 = eventObservable.subscribe(([arg1, arg2]) => {
expect(arg1).toBe("Hi!");
expect(arg2).toBe(2);
subscription2Called = true;
});
event.fireEvent("Hi!", 2);
subscription1.unsubscribe();
subscription2.unsubscribe();
expect(event.activeListeners).toHaveLength(0);
expect(subscription1Called).toBeTruthy();
expect(subscription2Called).toBeTruthy();
});
});

View File

@@ -0,0 +1,39 @@
import { Observable } from "rxjs";
import { BrowserApi } from "./browser-api";
/**
* Converts a Chrome event to an Observable stream.
*
* @typeParam T - The type of the event arguments.
* @param event - The Chrome event to convert.
* @returns An Observable stream of the event arguments.
*
* @remarks
* This function creates an Observable stream that listens to a Chrome event and emits its arguments
* whenever the event is triggered. If the event throws an error, the Observable will emit an error
* notification with the error message.
*
* @example
* ```typescript
* const onMessage = fromChromeEvent(chrome.runtime.onMessage);
* onMessage.subscribe((message) => console.log('Received message:', message));
* ```
*/
export function fromChromeEvent<T extends unknown[]>(
event: chrome.events.Event<(...args: T) => void>
): Observable<T> {
return new Observable<T>((subscriber) => {
const handler = (...args: T) => {
if (chrome.runtime.lastError) {
subscriber.error(chrome.runtime.lastError);
return;
}
subscriber.next(args);
};
BrowserApi.addListener(event, handler);
return () => BrowserApi.removeListener(event, handler);
});
}

View File

@@ -1,7 +1,39 @@
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { Observable, mergeMap } from "rxjs";
import {
AbstractStorageService,
StorageUpdate,
StorageUpdateType,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { fromChromeEvent } from "../../browser/from-chrome-event";
export default abstract class AbstractChromeStorageService implements AbstractStorageService {
protected abstract chromeStorageApi: chrome.storage.StorageArea;
constructor(protected chromeStorageApi: chrome.storage.StorageArea) {}
get updates$(): Observable<StorageUpdate> {
return fromChromeEvent(this.chromeStorageApi.onChanged).pipe(
mergeMap(([changes]) => {
return Object.entries(changes).map(([key, change]) => {
// The `newValue` property isn't on the StorageChange object
// when the change was from a remove. Similarly a check of the `oldValue`
// could be used to tell if the operation was the first creation of this key
// but we currently do not differentiate that.
// Ref: https://developer.chrome.com/docs/extensions/reference/storage/#type-StorageChange
// Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/StorageChange
const updateType: StorageUpdateType = "newValue" in change ? "save" : "remove";
return {
key: key,
// For removes this property will not exist but then it will just be
// undefined which is fine.
value: change.newValue,
updateType: updateType,
};
});
})
);
}
async get<T>(key: string): Promise<T> {
return new Promise((resolve) => {
@@ -22,11 +54,7 @@ export default abstract class AbstractChromeStorageService implements AbstractSt
async save(key: string, obj: any): Promise<void> {
if (obj == null) {
// Fix safari not liking null in set
return new Promise<void>((resolve) => {
this.chromeStorageApi.remove(key, () => {
resolve();
});
});
return this.remove(key);
}
if (obj instanceof Set) {

View File

@@ -1,5 +1,7 @@
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
export default class BrowserLocalStorageService extends AbstractChromeStorageService {
protected chromeStorageApi = chrome.storage.local;
constructor() {
super(chrome.storage.local);
}
}

View File

@@ -1,5 +1,7 @@
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
protected chromeStorageApi = chrome.storage.session;
constructor() {
super(chrome.storage.session);
}
}

View File

@@ -1,7 +1,11 @@
import { Subject } from "rxjs";
import { Jsonify } from "type-fest";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import {
AbstractMemoryStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@@ -22,6 +26,7 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
private cache = new Map<string, unknown>();
private localStorage = new BrowserLocalStorageService();
private sessionStorage = new BrowserMemoryStorageService();
private updatesSubject = new Subject<StorageUpdate>();
constructor(
private encryptService: EncryptService,
@@ -30,6 +35,10 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
super();
}
get updates$() {
return this.updatesSubject.asObservable();
}
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
if (this.cache.has(key)) {
return this.cache.get(key) as T;

View File

@@ -219,6 +219,9 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
});
this.windowClosed$ = fromEventPattern(
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
// and test that it doesn't break. Tracking Ticket: https://bitwarden.atlassian.net/browse/PM-4735
// eslint-disable-next-line no-restricted-syntax
(handler: any) => chrome.windows.onRemoved.addListener(handler),
(handler: any) => chrome.windows.onRemoved.removeListener(handler)
);

View File

@@ -5,10 +5,14 @@ import * as lowdb from "lowdb";
import * as FileSync from "lowdb/adapters/FileSync";
import * as lock from "proper-lockfile";
import { OperationOptions } from "retry";
import { Subject } from "rxjs";
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import {
AbstractStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { sequentialize } from "@bitwarden/common/platform/misc/sequentialize";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -24,6 +28,7 @@ export class LowdbStorageService implements AbstractStorageService {
private db: lowdb.LowdbSync<any>;
private defaults: any;
private ready = false;
private updatesSubject = new Subject<StorageUpdate>();
constructor(
protected logService: LogService,
@@ -102,6 +107,10 @@ export class LowdbStorageService implements AbstractStorageService {
this.ready = true;
}
get updates$() {
return this.updatesSubject.asObservable();
}
async get<T>(key: string): Promise<T> {
await this.waitForReady();
return this.lockDbFile(() => {
@@ -119,21 +128,23 @@ export class LowdbStorageService implements AbstractStorageService {
return this.get(key).then((v) => v != null);
}
async save(key: string, obj: any): Promise<any> {
async save(key: string, obj: any): Promise<void> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
this.db.set(key, obj).write();
this.updatesSubject.next({ key, value: obj, updateType: "save" });
this.logService.debug(`Successfully wrote ${key} to db`);
return;
});
}
async remove(key: string): Promise<any> {
async remove(key: string): Promise<void> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
this.db.unset(key).write();
this.updatesSubject.next({ key, value: null, updateType: "remove" });
this.logService.debug(`Successfully removed ${key} from db`);
return;
});

View File

@@ -1,3 +1,5 @@
import { throwError } from "rxjs";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
@@ -12,6 +14,12 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
private cryptoService: () => CryptoService
) {}
get updates$() {
return throwError(
() => new Error("Secure storage implementations cannot have their updates subscribed to.")
);
}
async get<T>(key: string): Promise<T> {
const value = await this.storageService.get<string>(this.makeProtectedStorageKey(key));
if (value == null) {
@@ -25,7 +33,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
return (await this.get(key)) != null;
}
async save(key: string, obj: any): Promise<any> {
async save(key: string, obj: any): Promise<void> {
if (obj == null) {
return this.remove(key);
}
@@ -37,8 +45,9 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
await this.storageService.save(this.makeProtectedStorageKey(key), protectedObj);
}
remove(key: string): Promise<any> {
return this.storageService.remove(this.makeProtectedStorageKey(key));
async remove(key: string): Promise<void> {
await this.storageService.remove(this.makeProtectedStorageKey(key));
return;
}
private async encrypt(plainValue: string): Promise<string> {

View File

@@ -1,7 +1,15 @@
import { throwError } from "rxjs";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
export class ElectronRendererSecureStorageService implements AbstractStorageService {
get updates$() {
return throwError(
() => new Error("Secure storage implementations cannot have their updates subscribed to.")
);
}
async get<T>(key: string, options?: StorageOptions): Promise<T> {
const val = await ipc.platform.passwords.get(key, options?.keySuffix ?? "");
return val != null ? (JSON.parse(val) as T) : null;
@@ -12,11 +20,11 @@ export class ElectronRendererSecureStorageService implements AbstractStorageServ
return !!val;
}
async save(key: string, obj: any, options?: StorageOptions): Promise<any> {
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
await ipc.platform.passwords.set(key, options?.keySuffix ?? "", JSON.stringify(obj));
}
async remove(key: string, options?: StorageOptions): Promise<any> {
async remove(key: string, options?: StorageOptions): Promise<void> {
await ipc.platform.passwords.delete(key, options?.keySuffix ?? "");
}
}

View File

@@ -1,6 +1,17 @@
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { Subject } from "rxjs";
import {
AbstractStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
export class ElectronRendererStorageService implements AbstractStorageService {
private updatesSubject = new Subject<StorageUpdate>();
get updates$() {
return this.updatesSubject.asObservable();
}
get<T>(key: string): Promise<T> {
return ipc.platform.storage.get(key);
}
@@ -9,11 +20,13 @@ export class ElectronRendererStorageService implements AbstractStorageService {
return ipc.platform.storage.has(key);
}
save(key: string, obj: any): Promise<any> {
return ipc.platform.storage.save(key, obj);
async save<T>(key: string, obj: T): Promise<void> {
await ipc.platform.storage.save(key, obj);
this.updatesSubject.next({ key, value: obj, updateType: "save" });
}
remove(key: string): Promise<any> {
return ipc.platform.storage.remove(key);
async remove(key: string): Promise<void> {
await ipc.platform.storage.remove(key);
this.updatesSubject.next({ key, value: null, updateType: "remove" });
}
}

View File

@@ -1,9 +1,13 @@
import * as fs from "fs";
import { ipcMain } from "electron";
import { Subject } from "rxjs";
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import {
AbstractStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
// See: https://github.com/sindresorhus/electron-store/blob/main/index.d.ts
interface ElectronStoreOptions {
@@ -35,6 +39,7 @@ type Options = BaseOptions<"get"> | BaseOptions<"has"> | SaveOptions | BaseOptio
export class ElectronStorageService implements AbstractStorageService {
private store: ElectronStore;
private updatesSubject = new Subject<StorageUpdate>();
constructor(dir: string, defaults = {}) {
if (!fs.existsSync(dir)) {
@@ -60,6 +65,10 @@ export class ElectronStorageService implements AbstractStorageService {
});
}
get updates$() {
return this.updatesSubject.asObservable();
}
get<T>(key: string): Promise<T> {
const val = this.store.get(key) as T;
return Promise.resolve(val != null ? val : null);
@@ -75,11 +84,13 @@ export class ElectronStorageService implements AbstractStorageService {
obj = Array.from(obj);
}
this.store.set(key, obj);
this.updatesSubject.next({ key, value: obj, updateType: "save" });
return Promise.resolve();
}
remove(key: string): Promise<void> {
this.store.delete(key);
this.updatesSubject.next({ key, value: null, updateType: "remove" });
return Promise.resolve();
}
}

View File

@@ -1,15 +1,25 @@
import { Injectable } from "@angular/core";
import { Subject } from "rxjs";
import { HtmlStorageLocation } from "@bitwarden/common/enums";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import {
AbstractStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
@Injectable()
export class HtmlStorageService implements AbstractStorageService {
private updatesSubject = new Subject<StorageUpdate>();
get defaultOptions(): StorageOptions {
return { htmlStorageLocation: HtmlStorageLocation.Session };
}
get updates$() {
return this.updatesSubject.asObservable();
}
get<T>(key: string, options: StorageOptions = this.defaultOptions): Promise<T> {
let json: string = null;
switch (options.htmlStorageLocation) {
@@ -52,6 +62,7 @@ export class HtmlStorageService implements AbstractStorageService {
window.sessionStorage.setItem(key, json);
break;
}
this.updatesSubject.next({ key, value: obj, updateType: "save" });
return Promise.resolve();
}
@@ -65,6 +76,7 @@ export class HtmlStorageService implements AbstractStorageService {
window.sessionStorage.removeItem(key);
break;
}
this.updatesSubject.next({ key, value: null, updateType: "remove" });
return Promise.resolve();
}
}