Merge pull request #8524 from backstage/mob/storageapi

core-plugin-api: refactor StorageApi to better cater to async storage implementations
This commit is contained in:
Patrik Oldsberg
2021-12-21 13:36:53 +01:00
committed by GitHub
12 changed files with 367 additions and 79 deletions
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/core-plugin-api': minor
---
**BREAKING CHANGE** The `StorageApi` has received several updates that fills in gaps for some use-cases and makes it easier to avoid mistakes:
- The `StorageValueChange` type has been renamed to `StorageValueSnapshot`, the `newValue` property has been renamed to `value`, the stored value type has been narrowed to `JsonValue`, and it has received a new `presence` property that is `'unknown'`, `'absent'`, or `'present'`.
- The `get` method has been deprecated in favor of a new `snapshot` method, which returns a `StorageValueSnapshot`.
- The `observe$` method has had its contract changed. It should now emit values when the `presence` of a key changes, this may for example happen when remotely stored values are requested on page load and the presence switches from `'unknown'` to either `'absent'` or `'present'`.
The above changes have been made with deprecations in place to maintain much of the backwards compatibility for consumers of the `StorageApi`. The only breaking change is the narrowing of the stored value type, which may in some cases require the addition of an explicit type parameter to the `get` and `observe$` methods.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': minor
---
Updated `WebStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': minor
---
Updated `MockStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`.
+5 -2
View File
@@ -39,6 +39,7 @@ import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';
import { OAuthApi } from '@backstage/core-plugin-api';
import { OAuthRequestApi } from '@backstage/core-plugin-api';
@@ -59,7 +60,7 @@ import { RouteRef } from '@backstage/core-plugin-api';
import { SessionApi } from '@backstage/core-plugin-api';
import { SessionState } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageValueChange } from '@backstage/core-plugin-api';
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
import { SubRouteRef } from '@backstage/core-plugin-api';
// @public
@@ -606,10 +607,12 @@ export class WebStorage implements StorageApi {
// (undocumented)
get<T>(key: string): T | undefined;
// (undocumented)
observe$<T>(key: string): Observable<StorageValueChange<T>>;
observe$<T>(key: string): Observable<StorageValueSnapshot<T>>;
// (undocumented)
remove(key: string): Promise<void>;
// (undocumented)
set<T>(key: string, data: T): Promise<void>;
// (undocumented)
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
```
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { WebStorage } from './WebStorage';
import { ErrorApi, StorageApi } from '@backstage/core-plugin-api';
@@ -33,6 +34,12 @@ describe('WebStorage Storage API', () => {
const storage = createWebStorage();
expect(storage.get('myfakekey')).toBeUndefined();
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
it('should allow the setting and getting of the simple data structures', async () => {
@@ -44,9 +51,27 @@ describe('WebStorage Storage API', () => {
expect(storage.get('myfakekey')).toBe('helloimastring');
expect(storage.get('mysecondfakekey')).toBe(1234);
expect(storage.get('mythirdfakekey')).toBe(true);
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'present',
value: 'helloimastring',
newValue: 'helloimastring',
});
expect(storage.snapshot('mysecondfakekey')).toEqual({
key: 'mysecondfakekey',
presence: 'present',
value: 1234,
newValue: 1234,
});
expect(storage.snapshot('mythirdfakekey')).toEqual({
key: 'mythirdfakekey',
presence: 'present',
value: true,
newValue: true,
});
});
it('should allow setting of complex datastructures', async () => {
it('should allow setting of complex data structures', async () => {
const storage = createWebStorage();
const mockData = {
@@ -57,6 +82,12 @@ describe('WebStorage Storage API', () => {
await storage.set('myfakekey', mockData);
expect(storage.get('myfakekey')).toEqual(mockData);
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'present',
value: mockData,
newValue: mockData,
});
});
it('should subscribe to key changes when setting a new value', async () => {
@@ -67,10 +98,12 @@ describe('WebStorage Storage API', () => {
const mockData = { hello: 'im a great new value' };
await new Promise<void>(resolve => {
storage.observe$<String>('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
storage.observe$<typeof mockData>('correctKey').subscribe({
next: snapshot => {
selectedKeyNextHandler(snapshot);
if (snapshot.presence === 'present') {
resolve();
}
},
});
@@ -79,10 +112,12 @@ describe('WebStorage Storage API', () => {
storage.set('correctKey', mockData);
});
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
expect(wrongKeyNextHandler).toHaveBeenCalledTimes(0);
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'present',
value: mockData,
newValue: mockData,
});
});
@@ -98,9 +133,11 @@ describe('WebStorage Storage API', () => {
await new Promise<void>(resolve => {
storage.observe$('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
next: snapshot => {
selectedKeyNextHandler(snapshot);
if (snapshot.presence === 'absent') {
resolve();
}
},
});
@@ -109,10 +146,12 @@ describe('WebStorage Storage API', () => {
storage.remove('correctKey');
});
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
expect(wrongKeyNextHandler).toHaveBeenCalledTimes(0);
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
@@ -130,9 +169,24 @@ describe('WebStorage Storage API', () => {
expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName));
expect(firstStorage.get(keyName)).toBe('boop');
expect(secondStorage.get(keyName)).toBe('deerp');
expect(firstStorage.snapshot(keyName)).not.toEqual(
secondStorage.snapshot(keyName),
);
expect(firstStorage.snapshot(keyName)).toEqual({
key: keyName,
presence: 'present',
value: 'boop',
newValue: 'boop',
});
expect(secondStorage.snapshot(keyName)).toEqual({
key: keyName,
presence: 'present',
value: 'deerp',
newValue: 'deerp',
});
});
it('should not clash with other namesapces when creating buckets', async () => {
it('should not clash with other namespaces when creating buckets', async () => {
const rootStorage = createWebStorage();
// when getting key test2 it will translate to /profile/something/deep/test2
@@ -145,7 +199,9 @@ describe('WebStorage Storage API', () => {
await firstStorage.set('test2', { error: true });
expect(secondStorage.get('deep/test2')).toBe(undefined);
expect(secondStorage.snapshot('deep/test2')).toMatchObject({
presence: 'absent',
});
});
it('should call the error api when the json can not be parsed in local storage', async () => {
@@ -155,9 +211,14 @@ describe('WebStorage Storage API', () => {
localStorage.setItem('/Test/Mock/Thing/key', '{smd: asdouindA}');
const value = rootStorage.get('key');
const value = rootStorage.snapshot('key');
expect(value).toBe(undefined);
expect(value).toEqual({
key: 'key',
presence: 'absent',
value: undefined,
newValue: undefined,
});
expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error));
expect(mockErrorApi.post).toHaveBeenCalledWith(
expect.objectContaining({
@@ -166,11 +227,35 @@ describe('WebStorage Storage API', () => {
);
});
it('should return a singleton for the same namespace and same bucket', async () => {
it('should return a stable reference for the same namespace and same bucket', async () => {
const rootStorage = createWebStorage({
namespace: '/Test/Mock/Thing/Thing ',
});
expect(rootStorage.forBucket('test')).toBe(rootStorage.forBucket('test'));
});
it('should freeze the snapshot value', async () => {
const storage = createWebStorage();
const data = { foo: 'bar', baz: [{ foo: 'bar' }] };
storage.set('foo', data);
const snapshot = storage.snapshot<typeof data>('foo');
expect(snapshot.value).not.toBe(data);
if (snapshot.presence !== 'present') {
throw new Error('Invalid presence');
}
expect(() => {
snapshot.value.foo = 'buzz';
}).toThrow(/Cannot assign to read only property/);
expect(() => {
snapshot.value.baz[0].foo = 'buzz';
}).toThrow(/Cannot assign to read only property/);
expect(() => {
snapshot.value.baz.push({ foo: 'buzz' });
}).toThrow(/Cannot add property 1, object is not extensible/);
});
});
@@ -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<string, WebStorage>();
@@ -43,16 +43,29 @@ export class WebStorage implements StorageApi {
}
get<T>(key: string): T | undefined {
return this.snapshot(key).value as T | undefined;
}
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T> {
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<T>(key: string, data: T): Promise<void> {
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<void> {
localStorage.removeItem(this.getKeyName(key));
this.notifyChanges({ key, newValue: undefined });
this.notifyChanges(key);
}
observe$<T>(key: string): Observable<StorageValueChange<T>> {
observe$<T>(key: string): Observable<StorageValueSnapshot<T>> {
return this.observable.filter(({ key: messageKey }) => messageKey === key);
}
@@ -81,22 +94,23 @@ export class WebStorage implements StorageApi {
return `${this.namespace}/${encodeURIComponent(key)}`;
}
private notifyChanges<T>(message: StorageValueChange<T>) {
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<StorageValueChange>
ZenObservable.SubscriptionObserver<StorageValueSnapshot<JsonValue>>
>();
private readonly observable = new ObservableImpl<StorageValueChange>(
subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
},
);
private readonly observable = new ObservableImpl<
StorageValueSnapshot<JsonValue>
>(subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
});
}
+25 -7
View File
@@ -10,6 +10,7 @@ import { ComponentType } from 'react';
import { Config } from '@backstage/config';
import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api';
import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { Observable } from '@backstage/types';
import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
@@ -762,20 +763,37 @@ export type SignInResult = {
// @public
export interface StorageApi {
forBucket(name: string): StorageApi;
get<T>(key: string): T | undefined;
observe$<T>(key: string): Observable<StorageValueChange<T>>;
// @deprecated
get<T extends JsonValue>(key: string): T | undefined;
observe$<T extends JsonValue>(
key: string,
): Observable<StorageValueSnapshot<T>>;
remove(key: string): Promise<void>;
set(key: string, data: any): Promise<void>;
set<T extends JsonValue>(key: string, data: T): Promise<void>;
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
// @public
export const storageApiRef: ApiRef<StorageApi>;
// @public @deprecated (undocumented)
export type StorageValueChange<TValue extends JsonValue> =
StorageValueSnapshot<TValue>;
// @public
export type StorageValueChange<T = any> = {
key: string;
newValue?: T;
};
export type StorageValueSnapshot<TValue extends JsonValue> =
| {
key: string;
presence: 'unknown' | 'absent';
value?: undefined;
newValue?: undefined;
}
| {
key: string;
presence: 'present';
value: TValue;
newValue?: TValue;
};
// @public
export type SubRouteRef<Params extends AnyParams = any> = {
@@ -15,55 +15,108 @@
*/
import { ApiRef, createApiRef } from '../system';
import { Observable } from '@backstage/types';
import { JsonValue, Observable } from '@backstage/types';
/**
* Describes a value change event.
* A snapshot in time of the current known value of a storage key.
*
* @public
*/
export type StorageValueChange<T = any> = {
key: string;
newValue?: T;
};
export type StorageValueSnapshot<TValue extends JsonValue> =
| {
key: string;
presence: 'unknown' | 'absent';
value?: undefined;
/** @deprecated Use `value` instead */
newValue?: undefined;
}
| {
key: string;
presence: 'present';
value: TValue;
/** @deprecated Use `value` instead */
newValue?: TValue;
};
/**
* Provides key-value persistence API.
* @public
* @deprecated Use StorageValueSnapshot instead
*/
export type StorageValueChange<TValue extends JsonValue> =
StorageValueSnapshot<TValue>;
/**
* Provides a key-value persistence API.
*
* @public
*/
export interface StorageApi {
/**
* Create a bucket to store data in.
*
* @param name - Namespace for the storage to be stored under,
* will inherit previous namespaces too
* will inherit previous namespaces too
*/
forBucket(name: string): StorageApi;
/**
* Get the current value for persistent data, use observe$ to be notified of updates.
*
* @deprecated Use `snapshot` instead.
* @param key - Unique key associated with the data.
*/
get<T>(key: string): T | undefined;
get<T extends JsonValue>(key: string): T | undefined;
/**
* Remove persistent data.
*
* @param key - Unique key associated with the data.
*/
remove(key: string): Promise<void>;
/**
* Save persistent data, and emit messages to anyone that is using observe$ for this key
* Save persistent data, and emit messages to anyone that is using
* {@link StorageApi.observe$} for this key.
*
* @param key - Unique key associated with the data.
* @param data - The data to be stored under the key.
*/
set(key: string, data: any): Promise<void>;
set<T extends JsonValue>(key: string, data: T): Promise<void>;
/**
* Observe changes on a particular key in the bucket
* Observe the value over time for a particular key in the current bucket.
*
* @remarks
*
* 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.
*
* 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
*/
observe$<T>(key: string): Observable<StorageValueChange<T>>;
observe$<T extends JsonValue>(
key: string,
): Observable<StorageValueSnapshot<T>>;
/**
* Returns an immediate snapshot value for the given key, if possible.
*
* @remarks
*
* Combine with {@link StorageApi.observe$} to get notified of value changes.
*
* Note that this method is synchronous, and some underlying storages may be
* unable to retrieve a value using this method - the result may or may not
* consistently have a presence of 'unknown'. Use {@link StorageApi.observe$}
* to be sure to receive an actual value eventually.
*/
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
/**
+5 -2
View File
@@ -12,13 +12,14 @@ import { ErrorApi } from '@backstage/core-plugin-api';
import { ErrorApiError } from '@backstage/core-plugin-api';
import { ErrorApiErrorContext } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { JsonValue } from '@backstage/types';
import { Observable } from '@backstage/types';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RenderResult } from '@testing-library/react';
import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageValueChange } from '@backstage/core-plugin-api';
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
// @public
export type AsyncLogCollector = () => Promise<void>;
@@ -81,11 +82,13 @@ export class MockStorageApi implements StorageApi {
// (undocumented)
get<T>(key: string): T | undefined;
// (undocumented)
observe$<T>(key: string): Observable<StorageValueChange<T>>;
observe$<T>(key: string): Observable<StorageValueSnapshot<T>>;
// (undocumented)
remove(key: string): Promise<void>;
// (undocumented)
set<T>(key: string, data: T): Promise<void>;
// (undocumented)
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
// @public
@@ -25,6 +25,12 @@ describe('WebStorage Storage API', () => {
const storage = createMockStorage();
expect(storage.get('myfakekey')).toBeUndefined();
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
it('should allow the setting and getting of the simple data structures', async () => {
@@ -36,6 +42,24 @@ describe('WebStorage Storage API', () => {
expect(storage.get('myfakekey')).toBe('helloimastring');
expect(storage.get('mysecondfakekey')).toBe(1234);
expect(storage.get('mythirdfakekey')).toBe(true);
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'present',
value: 'helloimastring',
newValue: 'helloimastring',
});
expect(storage.snapshot('mysecondfakekey')).toEqual({
key: 'mysecondfakekey',
presence: 'present',
value: 1234,
newValue: 1234,
});
expect(storage.snapshot('mythirdfakekey')).toEqual({
key: 'mythirdfakekey',
presence: 'present',
value: true,
newValue: true,
});
});
it('should allow setting of complex datastructures', async () => {
@@ -49,6 +73,12 @@ describe('WebStorage Storage API', () => {
await storage.set('myfakekey', mockData);
expect(storage.get('myfakekey')).toEqual(mockData);
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'present',
value: mockData,
newValue: mockData,
});
});
it('should subscribe to key changes when setting a new value', async () => {
@@ -59,7 +89,7 @@ describe('WebStorage Storage API', () => {
const mockData = { hello: 'im a great new value' };
await new Promise<void>(resolve => {
storage.observe$<String>('correctKey').subscribe({
storage.observe$<typeof mockData>('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
@@ -75,6 +105,8 @@ describe('WebStorage Storage API', () => {
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'present',
value: mockData,
newValue: mockData,
});
});
@@ -105,6 +137,8 @@ describe('WebStorage Storage API', () => {
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
@@ -122,6 +156,21 @@ describe('WebStorage Storage API', () => {
expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName));
expect(firstStorage.get(keyName)).toBe('boop');
expect(secondStorage.get(keyName)).toBe('deerp');
expect(firstStorage.snapshot(keyName)).not.toEqual(
secondStorage.snapshot(keyName),
);
expect(firstStorage.snapshot(keyName)).toEqual({
key: keyName,
presence: 'present',
value: 'boop',
newValue: 'boop',
});
expect(secondStorage.snapshot(keyName)).toEqual({
key: keyName,
presence: 'present',
value: 'deerp',
newValue: 'deerp',
});
});
it('should not clash with other namespaces when creating buckets', async () => {
@@ -138,6 +187,9 @@ describe('WebStorage Storage API', () => {
await firstStorage.set('test2', { error: true });
expect(secondStorage.get('deep/test2')).toBe(undefined);
expect(secondStorage.snapshot('deep/test2')).toMatchObject({
presence: 'absent',
});
});
it('should not reuse storage instances between different rootStorages', async () => {
@@ -151,5 +203,17 @@ describe('WebStorage Storage API', () => {
expect(firstStorage.get('test2')).toBe(true);
expect(secondStorage.get('test2')).toBe(undefined);
expect(firstStorage.snapshot('test2')).toEqual({
key: 'test2',
presence: 'present',
value: true,
newValue: true,
});
expect(secondStorage.snapshot('test2')).toEqual({
key: 'test2',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
});
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { StorageApi, StorageValueChange } from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { StorageApi, StorageValueSnapshot } from '@backstage/core-plugin-api';
import { JsonValue, Observable } from '@backstage/types';
import ObservableImpl from 'zen-observable';
/**
@@ -62,20 +62,48 @@ export class MockStorageApi implements StorageApi {
}
get<T>(key: string): T | undefined {
return this.data[this.getKeyName(key)];
return this.snapshot(key).value as T | undefined;
}
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T> {
if (this.data.hasOwnProperty(this.getKeyName(key))) {
const data = this.data[this.getKeyName(key)];
return {
key,
presence: 'present',
value: data,
newValue: data,
};
}
return {
key,
presence: 'absent',
value: undefined,
newValue: undefined,
};
}
async set<T>(key: string, data: T): Promise<void> {
this.data[this.getKeyName(key)] = data;
this.notifyChanges({ key, newValue: data });
this.notifyChanges({
key,
presence: 'present',
value: data,
newValue: data,
});
}
async remove(key: string): Promise<void> {
delete this.data[this.getKeyName(key)];
this.notifyChanges({ key, newValue: undefined });
this.notifyChanges({
key,
presence: 'absent',
value: undefined,
newValue: undefined,
});
}
observe$<T>(key: string): Observable<StorageValueChange<T>> {
observe$<T>(key: string): Observable<StorageValueSnapshot<T>> {
return this.observable.filter(({ key: messageKey }) => messageKey === key);
}
@@ -83,22 +111,22 @@ export class MockStorageApi implements StorageApi {
return `${this.namespace}/${encodeURIComponent(key)}`;
}
private notifyChanges<T>(message: StorageValueChange<T>) {
private notifyChanges<T>(message: StorageValueSnapshot<T>) {
for (const subscription of this.subscribers) {
subscription.next(message);
}
}
private subscribers = new Set<
ZenObservable.SubscriptionObserver<StorageValueChange>
ZenObservable.SubscriptionObserver<StorageValueSnapshot<JsonValue>>
>();
private readonly observable = new ObservableImpl<StorageValueChange>(
subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
},
);
private readonly observable = new ObservableImpl<
StorageValueSnapshot<JsonValue>
>(subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
});
}
@@ -41,8 +41,7 @@ export async function performMigrationToTheNewBucket({
// nothing to do
return;
}
const targetEntities = new Set(target.get('entityRefs') ?? []);
const targetEntities = new Set(target.get<string[]>('entityRefs') ?? []);
oldStarredEntities
.filter(isString)