diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts index b252e6b726..c4cffaf184 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -16,10 +16,10 @@ import { StorageApi, - StorageValueChange, + StorageValueSnapshot, ErrorApi, } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/types'; +import { JsonValue, Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; const buckets = new Map(); @@ -43,16 +43,29 @@ export class WebStorage implements StorageApi { } get(key: string): T | undefined { + return this.snapshot(key).value as T | undefined; + } + + snapshot(key: string): StorageValueSnapshot { + let value = undefined; + let presence: 'present' | 'absent' = 'absent'; try { - const storage = JSON.parse(localStorage.getItem(this.getKeyName(key))!); - return storage ?? undefined; + const item = localStorage.getItem(this.getKeyName(key)); + if (item) { + value = JSON.parse(item, (_key, val) => { + if (typeof val === 'object' && val !== null) { + Object.freeze(val); + } + return val; + }); + presence = 'present'; + } } catch (e) { this.errorApi.post( new Error(`Error when parsing JSON config from storage for: ${key}`), ); } - - return undefined; + return { key, value, newValue: value, presence }; } forBucket(name: string): WebStorage { @@ -64,16 +77,16 @@ export class WebStorage implements StorageApi { } async set(key: string, data: T): Promise { - localStorage.setItem(this.getKeyName(key), JSON.stringify(data, null, 2)); - this.notifyChanges({ key, newValue: data }); + localStorage.setItem(this.getKeyName(key), JSON.stringify(data)); + this.notifyChanges(key); } async remove(key: string): Promise { localStorage.removeItem(this.getKeyName(key)); - this.notifyChanges({ key, newValue: undefined }); + this.notifyChanges(key); } - observe$(key: string): Observable> { + observe$(key: string): Observable> { return this.observable.filter(({ key: messageKey }) => messageKey === key); } @@ -81,22 +94,23 @@ export class WebStorage implements StorageApi { return `${this.namespace}/${encodeURIComponent(key)}`; } - private notifyChanges(message: StorageValueChange) { + private notifyChanges(key: string) { + const snapshot = this.snapshot(key); for (const subscription of this.subscribers) { - subscription.next(message); + subscription.next(snapshot); } } private subscribers = new Set< - ZenObservable.SubscriptionObserver + ZenObservable.SubscriptionObserver> >(); - private readonly observable = new ObservableImpl( - subscriber => { - this.subscribers.add(subscriber); - return () => { - this.subscribers.delete(subscriber); - }; - }, - ); + private readonly observable = new ObservableImpl< + StorageValueSnapshot + >(subscriber => { + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }); } diff --git a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts index 25ec5d8131..8dcfe6739a 100644 --- a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts @@ -85,12 +85,15 @@ export interface StorageApi { * * @remarks * - * The returned observable will immediately emit a value snapshot when - * subscribed to, even if the presence of the value is unknown at that time. + * The observable will only emit values when the value changes in the underlying + * storage, although multiple values with the same shape may be emitted in a row. * - * The values emitted by the observe method aim to keep reference equality - * across snapshots if the value is unchanged. This means that it is important - * not to mutate the returned values, and they may be frozen as a precaution. + * If a {@link StorageApi.snapshot} of a key is retrieved and the presence is + * `'unknown'`, then you are guaranteed to receive a snapshot with a known + * presence, as long as you observe the key within the same tick. + * + * Since the emitted values are shared across all subscribers, it is important + * not to mutate the returned values. The values may be frozen as a precaution. * * @param key - Unique key associated with the data */