diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 616e18601af..6155f6abe50 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs"; +import { Subject, defer, filter, firstValueFrom, map, merge, shareReplay, timeout } from "rxjs"; import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; import { @@ -103,7 +103,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; -import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; @@ -262,7 +261,6 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; -import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { OffscreenStorageService } from "../platform/storage/offscreen-storage.service"; import { SyncServiceListener } from "../platform/sync/sync-service.listener"; @@ -463,20 +461,9 @@ export default class MainBackground { this.offscreenDocumentService, ); - this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used - - if (BrowserApi.isManifestVersion(3)) { - // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3 - this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session - this.memoryStorageService = this.memoryStorageForStateProviders; - } else { - this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory - this.memoryStorageService = this.memoryStorageForStateProviders; - } - if (BrowserApi.isManifestVersion(3)) { // Creates a session key for mv3 storage of large memory items - const sessionKey = new Lazy(async () => { + const sessionKey = defer(async () => { // Key already in session storage const sessionStorage = new BrowserMemoryStorageService(); const existingKey = await sessionStorage.get("session-key"); @@ -495,7 +482,7 @@ export default class MainBackground { ); await sessionStorage.save("session-key", derivedKey); return derivedKey; - }); + }).pipe(shareReplay({ bufferSize: 1, refCount: false })); this.largeObjectMemoryStorageForStateProviders = new LocalBackedSessionStorageService( sessionKey, diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 3d45cf5fd7d..7c68c8ba538 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { filter, mergeMap } from "rxjs"; +import { concat, filter, map, mergeMap, Observable, share } from "rxjs"; import { AbstractStorageService, @@ -41,9 +41,14 @@ export const objToStore = (obj: any) => { export default abstract class AbstractChromeStorageService implements AbstractStorageService, ObservableStorageService { + onChanged$: Observable<{ [key: string]: chrome.storage.StorageChange }>; updates$; constructor(protected chromeStorageApi: chrome.storage.StorageArea) { + this.onChanged$ = fromChromeEvent(this.chromeStorageApi.onChanged).pipe( + map(([change]) => change), + share(), + ); this.updates$ = fromChromeEvent(this.chromeStorageApi.onChanged).pipe( filter(([changes]) => { // Our storage services support changing only one key at a time. If more are changed, it's due to @@ -75,6 +80,32 @@ export default abstract class AbstractChromeStorageService return true; } + get$(key: string): Observable { + const initialValue$ = new Observable((subscriber) => { + this.chromeStorageApi.get(key, (obj) => { + if (chrome.runtime.lastError) { + subscriber.error(chrome.runtime.lastError); + return; + } + + if (obj != null && obj[key] != null) { + subscriber.next(this.processGetObject(obj[key])); + } else { + subscriber.next(null); + } + + subscriber.complete(); + }); + }); + + const keyUpdates$: Observable = this.onChanged$.pipe( + filter((change) => change[key] != null), + map((change) => change[key].newValue ?? null), + ); + + return concat(initialValue$, keyUpdates$); + } + async get(key: string): Promise { return new Promise((resolve, reject) => { this.chromeStorageApi.get(key, (obj) => { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 949ecebde8a..7dd6128eec2 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -1,9 +1,9 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { FakeStorageService, makeEncString } from "@bitwarden/common/spec"; @@ -28,7 +28,7 @@ describe("LocalBackedSessionStorage", () => { logService = mock(); sut = new LocalBackedSessionStorageService( - new Lazy(async () => sessionKey), + of(sessionKey), localStorage, encryptService, platformUtilsService, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index bdc0bed80c0..bd2b1dab3e1 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Subject } from "rxjs"; +import { concat, EMPTY, firstValueFrom, Observable, Subject } from "rxjs"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -11,7 +11,6 @@ import { StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { compareValues } from "@bitwarden/common/platform/misc/compare-values"; -import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -31,7 +30,7 @@ export class LocalBackedSessionStorageService updates$ = this.updatesSubject.asObservable(); constructor( - private readonly sessionKey: Lazy>, + private readonly sessionKey$: Observable, private readonly localStorage: AbstractStorageService, private readonly encryptService: EncryptService, private readonly platformUtilsService: PlatformUtilsService, @@ -71,12 +70,28 @@ export class LocalBackedSessionStorageService return this.cache[key] as T; } - const value = await this.getLocalSessionValue(await this.sessionKey.get(), key); + const value = await this.getLocalSessionValue(await firstValueFrom(this.sessionKey$), key); this.cache[key] = value; return value as T; } + get$(key: string) { + const initialValue$ = new Observable((subscriber) => { + // + const cachedValue = this.cache[key] as T; + if (cachedValue !== undefined) { + subscriber.next(cachedValue); + subscriber.complete(); + } + + // Get value from session + }); + + // TODO: Connect something to get updates from elsewhere + return concat(initialValue$, EMPTY); + } + async has(key: string): Promise { return (await this.get(key)) != null; } @@ -104,13 +119,13 @@ export class LocalBackedSessionStorageService this.cache[key] = obj; await this.updateLocalSessionValue(key, obj); - this.updatesSubject.next({ key, updateType: "save" }); + // this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise { this.cache[key] = null; await this.updateLocalSessionValue(key, null); - this.updatesSubject.next({ key, updateType: "remove" }); + // this.updatesSubject.next({ key, updateType: "remove" }); } private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise { @@ -140,7 +155,10 @@ export class LocalBackedSessionStorageService } const valueJson = JSON.stringify(value); - const encValue = await this.encryptService.encrypt(valueJson, await this.sessionKey.get()); + const encValue = await this.encryptService.encrypt( + valueJson, + await firstValueFrom(this.sessionKey$), + ); await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString); } diff --git a/apps/browser/src/platform/storage/background-memory-storage.service.ts b/apps/browser/src/platform/storage/background-memory-storage.service.ts index 9c0cac0d044..ca24372ae2f 100644 --- a/apps/browser/src/platform/storage/background-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/background-memory-storage.service.ts @@ -33,12 +33,6 @@ export class BackgroundMemoryStorageService extends MemoryStorageService { data: Array.from(Object.keys(this.store)), }); }); - this.updates$.subscribe((update) => { - this.broadcastMessage({ - action: "subject_update", - data: update, - }); - }); } private async onMessageFromForeground( diff --git a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts index bd6a52c82fe..9ec3e9466e3 100644 --- a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts @@ -1,7 +1,8 @@ -import { Observable, Subject, filter, firstValueFrom, map } from "rxjs"; +import { EMPTY, Observable, Subject, concat, filter, firstValueFrom, map } from "rxjs"; import { AbstractStorageService, + ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -11,7 +12,10 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; import { MemoryStoragePortMessage } from "./port-messages"; import { portName } from "./port-name"; -export class ForegroundMemoryStorageService extends AbstractStorageService { +export class ForegroundMemoryStorageService + extends AbstractStorageService + implements ObservableStorageService +{ private _port: chrome.runtime.Port; private _backgroundResponses$: Observable; private updatesSubject = new Subject(); @@ -19,13 +23,10 @@ export class ForegroundMemoryStorageService extends AbstractStorageService { get valuesRequireDeserialization(): boolean { return true; } - updates$; constructor(private partitionName?: string) { super(); - this.updates$ = this.updatesSubject.asObservable(); - let name = portName(chrome.storage.session); if (this.partitionName) { name = `${name}_${this.partitionName}`; @@ -59,6 +60,23 @@ export class ForegroundMemoryStorageService extends AbstractStorageService { async get(key: string): Promise { return await this.delegateToBackground("get", key); } + + get$(key: string) { + return concat( + new Observable((subscriber) => { + this.delegateToBackground("get", key) + .then((value) => { + subscriber.next(value); + subscriber.complete(); + }) + .catch((err) => { + subscriber.error(err); + }); + }), + EMPTY, // TODO: Make a connection to background to hear about updates + ); + } + async has(key: string): Promise { return await this.delegateToBackground("has", key); } @@ -104,7 +122,7 @@ export class ForegroundMemoryStorageService extends AbstractStorageService { private handleInitialize(data: string[]) { // TODO: this isn't a save, but we don't have a better indicator for this data.forEach((key) => { - this.updatesSubject.next({ key, updateType: "save" }); + // this.updatesSubject.next({ key, updateType: "save" }); }); } diff --git a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts index c462f24269c..03f1684d502 100644 --- a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts +++ b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts @@ -3,8 +3,6 @@ * @jest-environment ../../libs/shared/test.environment.ts */ -import { trackEmissions } from "@bitwarden/common/../spec/utils"; - import { mockPorts } from "../../../spec/mock-port.spec-util"; import { BackgroundMemoryStorageService } from "./background-memory-storage.service"; @@ -54,15 +52,15 @@ describe("foreground background memory storage interaction", () => { expect(actionSpy).toHaveBeenCalledWith(key); }); - test("background updates push to foreground", async () => { - const key = "key"; - const value = "value"; - const updateType = "save"; - const emissions = trackEmissions(foreground.updates$); - await background.save(key, value); + // test("background updates push to foreground", async () => { + // const key = "key"; + // const value = "value"; + // const updateType = "save"; + // const emissions = trackEmissions(foreground.updates$); + // await background.save(key, value); - expect(emissions).toEqual([{ key, updateType }]); - }); + // expect(emissions).toEqual([{ key, updateType }]); + // }); test("background should message only the requesting foreground", async () => { const secondForeground = new ForegroundMemoryStorageService(); diff --git a/apps/web/src/app/core/html-storage.service.ts b/apps/web/src/app/core/html-storage.service.ts index 318aed5e9f3..b00cd7e3c3d 100644 --- a/apps/web/src/app/core/html-storage.service.ts +++ b/apps/web/src/app/core/html-storage.service.ts @@ -1,19 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; -import { - AbstractStorageService, - StorageUpdate, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { HtmlStorageLocation } from "@bitwarden/common/platform/enums"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; @Injectable() export class HtmlStorageService implements AbstractStorageService { - private updatesSubject = new Subject(); - get defaultOptions(): StorageOptions { return { htmlStorageLocation: HtmlStorageLocation.Session }; } @@ -21,11 +15,6 @@ export class HtmlStorageService implements AbstractStorageService { get valuesRequireDeserialization(): boolean { return true; } - updates$; - - constructor() { - this.updates$ = this.updatesSubject.asObservable(); - } get(key: string, options: StorageOptions = this.defaultOptions): Promise { let json: string = null; @@ -69,7 +58,6 @@ export class HtmlStorageService implements AbstractStorageService { window.sessionStorage.setItem(key, json); break; } - this.updatesSubject.next({ key, updateType: "save" }); return Promise.resolve(); } @@ -83,7 +71,6 @@ export class HtmlStorageService implements AbstractStorageService { window.sessionStorage.removeItem(key); break; } - this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } } diff --git a/libs/common/spec/fake-storage.service.ts b/libs/common/spec/fake-storage.service.ts index c6d989c5abf..669ffab639c 100644 --- a/libs/common/spec/fake-storage.service.ts +++ b/libs/common/spec/fake-storage.service.ts @@ -1,5 +1,5 @@ import { MockProxy, mock } from "jest-mock-extended"; -import { Subject } from "rxjs"; +import { concat, filter, map, Observable, of, Subject } from "rxjs"; import { AbstractStorageService, @@ -20,11 +20,11 @@ export class FakeStorageService implements AbstractStorageService, ObservableSto * amount of calls. It is not recommended to use this to mock implementations as * they are not respected. */ - mock: MockProxy; + mock: MockProxy; constructor(initial?: Record) { this.store = initial ?? {}; - this.mock = mock(); + this.mock = mock(); } /** @@ -48,8 +48,15 @@ export class FakeStorageService implements AbstractStorageService, ObservableSto return this._valuesRequireDeserialization; } - get updates$() { - return this.updatesSubject.asObservable(); + get$(key: string): Observable { + this.mock.get$(key); + return concat( + of((this.store[key] as T) ?? null), + this.updatesSubject.pipe( + filter((update) => update.key == key), + map((update) => (this.store[key] as T) ?? null), + ), + ); } get(key: string, options?: StorageOptions): Promise { diff --git a/libs/common/src/platform/abstractions/storage.service.ts b/libs/common/src/platform/abstractions/storage.service.ts index 390d71ae2ad..bb7ee29652a 100644 --- a/libs/common/src/platform/abstractions/storage.service.ts +++ b/libs/common/src/platform/abstractions/storage.service.ts @@ -9,12 +9,7 @@ export type StorageUpdate = { }; 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; + get$(key: string): Observable; } export abstract class AbstractStorageService { diff --git a/libs/common/src/platform/services/memory-storage.service.ts b/libs/common/src/platform/services/memory-storage.service.ts index 52d38dec9bf..5b41acc9b73 100644 --- a/libs/common/src/platform/services/memory-storage.service.ts +++ b/libs/common/src/platform/services/memory-storage.service.ts @@ -1,19 +1,13 @@ // 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"; +import { AbstractStorageService } from "../abstractions/storage.service"; export class MemoryStorageService extends AbstractStorageService { protected store = new Map(); - private updatesSubject = new Subject(); get valuesRequireDeserialization(): boolean { return false; } - get updates$() { - return this.updatesSubject.asObservable(); - } get(key: string): Promise { if (this.store.has(key)) { @@ -35,13 +29,11 @@ export class MemoryStorageService extends AbstractStorageService { // 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 { this.store.delete(key); - this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } } diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts index 567de957e53..a0d6cb9e00d 100644 --- a/libs/common/src/platform/state/implementations/state-base.ts +++ b/libs/common/src/platform/state/implementations/state-base.ts @@ -1,18 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - Observable, - ReplaySubject, - defer, - filter, - firstValueFrom, - merge, - share, - switchMap, - tap, - timeout, - timer, -} from "rxjs"; +import { Observable, ReplaySubject, firstValueFrom, map, share, tap, timeout, timer } from "rxjs"; import { Jsonify } from "type-fest"; import { StorageKey } from "../../../types/state"; @@ -44,22 +32,16 @@ export abstract class StateBase> protected readonly keyDefinition: KeyDef, protected readonly logService: LogService, ) { - const storageUpdate$ = storageService.updates$.pipe( - filter((storageUpdate) => storageUpdate.key === key), - switchMap(async (storageUpdate) => { - if (storageUpdate.updateType === "remove") { - return null; + let state$ = this.storageService.get$(key).pipe( + map((value) => { + if (this.storageService.valuesRequireDeserialization) { + return this.keyDefinition.deserializer(value as Jsonify); } - return await getStoredValue(key, storageService, keyDefinition.deserializer); + return value; }), ); - let state$ = merge( - defer(() => getStoredValue(key, storageService, keyDefinition.deserializer)), - storageUpdate$, - ); - if (keyDefinition.debug.enableRetrievalLogging) { state$ = state$.pipe( tap({ diff --git a/libs/common/src/platform/state/storage/memory-storage.service.ts b/libs/common/src/platform/state/storage/memory-storage.service.ts index df3fe615626..b5644b06c3d 100644 --- a/libs/common/src/platform/state/storage/memory-storage.service.ts +++ b/libs/common/src/platform/state/storage/memory-storage.service.ts @@ -1,11 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Subject } from "rxjs"; +import { concat, filter, map, of, Subject } from "rxjs"; import { AbstractStorageService, ObservableStorageService, - StorageUpdate, + StorageUpdateType, } from "../../abstractions/storage.service"; export class MemoryStorageService @@ -13,14 +13,11 @@ export class MemoryStorageService implements ObservableStorageService { protected store: Record = {}; - private updatesSubject = new Subject(); + private updatesSubject = new Subject<{ key: string; updateType: StorageUpdateType }>(); get valuesRequireDeserialization(): boolean { return true; } - get updates$() { - return this.updatesSubject.asObservable(); - } get(key: string): Promise { const json = this.store[key]; @@ -31,6 +28,31 @@ export class MemoryStorageService return Promise.resolve(null); } + private getValue(key: string): T | null { + const json = this.store[key]; + if (json) { + return JSON.parse(json) as T; + } + + return null; + } + + get$(key: string) { + return concat( + of(this.getValue(key)), + this.updatesSubject.pipe( + filter((update) => update.key === key), + map((update) => { + if (update.updateType === "remove") { + return null; + } + + return this.getValue(key); + }), + ), + ); + } + async has(key: string): Promise { return (await this.get(key)) != null; } diff --git a/libs/common/src/platform/storage/primary-secondary-storage.service.ts b/libs/common/src/platform/storage/primary-secondary-storage.service.ts index df62010fdc1..c1c734a166a 100644 --- a/libs/common/src/platform/storage/primary-secondary-storage.service.ts +++ b/libs/common/src/platform/storage/primary-secondary-storage.service.ts @@ -5,7 +5,6 @@ export class PrimarySecondaryStorageService implements AbstractStorageService, ObservableStorageService { // Only follow the primary storage service as updates should all be done to both - updates$ = this.primaryStorageService.updates$; constructor( private readonly primaryStorageService: AbstractStorageService & ObservableStorageService, @@ -25,6 +24,11 @@ export class PrimarySecondaryStorageService return this.primaryStorageService.valuesRequireDeserialization; } + get$(key: string) { + // TODO: What is best here? + return this.primaryStorageService.get$(key); + } + async get(key: string, options?: StorageOptions): Promise { const primaryValue = await this.primaryStorageService.get(key, options); diff --git a/libs/common/src/platform/storage/window-storage.service.ts b/libs/common/src/platform/storage/window-storage.service.ts index ed4189c39c9..9b59b5368a6 100644 --- a/libs/common/src/platform/storage/window-storage.service.ts +++ b/libs/common/src/platform/storage/window-storage.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable, Subject } from "rxjs"; +import { concat, filter, map, Observable, of, Subject } from "rxjs"; import { AbstractStorageService, @@ -10,24 +10,35 @@ import { import { StorageOptions } from "../models/domain/storage-options"; export class WindowStorageService implements AbstractStorageService, ObservableStorageService { - private readonly updatesSubject = new Subject(); + private readonly updatesSubject = new Subject(); - updates$: Observable; - constructor(private readonly storage: Storage) { - this.updates$ = this.updatesSubject.asObservable(); - } + constructor(private readonly storage: Storage) {} get valuesRequireDeserialization(): boolean { return true; } - get(key: string, options?: StorageOptions): Promise { + get$(key: string): Observable { + return concat( + of(this.getValue(key)), + this.updatesSubject.pipe( + filter((update) => update.key === key), + map((update) => update.value as T), + ), + ); + } + + private getValue(key: string) { const jsonValue = this.storage.getItem(key); if (jsonValue != null) { - return Promise.resolve(JSON.parse(jsonValue) as T); + return JSON.parse(jsonValue) as T; } - return Promise.resolve(null); + return null; + } + + get(key: string, options?: StorageOptions): Promise { + return Promise.resolve(this.getValue(key)); } async has(key: string, options?: StorageOptions): Promise { @@ -44,12 +55,12 @@ export class WindowStorageService implements AbstractStorageService, ObservableS } this.storage.setItem(key, JSON.stringify(obj)); - this.updatesSubject.next({ key, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save", value: obj }); } remove(key: string, options?: StorageOptions): Promise { this.storage.removeItem(key); - this.updatesSubject.next({ key, updateType: "remove" }); + this.updatesSubject.next({ key, updateType: "remove", value: null }); return Promise.resolve(); }