diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts index 2f82ed5e88..d211f61b97 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -15,7 +15,13 @@ */ import { WebStorage } from './WebStorage'; -import { ErrorApi, StorageApi } from '@backstage/core-plugin-api'; +import { + ErrorApi, + StorageApi, + StorageValueSnapshot, +} from '@backstage/core-plugin-api'; +import { buckets } from './WebStorage'; +import { JsonValue } from '@backstage/types'; describe('WebStorage Storage API', () => { const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; @@ -30,6 +36,11 @@ describe('WebStorage Storage API', () => { ...args, }); }; + + afterEach(() => { + buckets.clear(); + }); + it('should return undefined for values which are unset', async () => { const storage = createWebStorage(); @@ -250,4 +261,50 @@ describe('WebStorage Storage API', () => { snapshot.value.baz.push({ foo: 'buzz' }); }).toThrow(/Cannot add property 1, object is not extensible/); }); + + it('should forward storage events accurately', async () => { + const storage = createWebStorage(); + const bucket = storage.forBucket('foo'); + + localStorage.setItem('/foo/bar%2Fbaz', '"x"'); + localStorage.setItem('/foo/bar/baz', '"y"'); + + expect(bucket.snapshot('bar/baz').value).toBe('x'); + expect(bucket.forBucket('bar').snapshot('baz').value).toBe('y'); + + const notifyPromise = new Promise>( + resolve => { + const subscription = bucket.observe$('bar/baz').subscribe({ + next: snapshot => { + resolve(snapshot); + subscription.unsubscribe(); + }, + }); + }, + ); + + await expect( + Promise.race([notifyPromise, 'not-yet-resolved']), + ).resolves.toBe('not-yet-resolved'); + + window.dispatchEvent(new StorageEvent('storage', { key: '/foo/bar/baz' })); + + localStorage.setItem('/foo/bar%2Fbaz', '"z"'); + + await expect( + Promise.race([notifyPromise, 'not-yet-resolved']), + ).resolves.toBe('not-yet-resolved'); + + window.dispatchEvent( + new StorageEvent('storage', { key: '/foo/bar%2Fbaz' }), + ); + + await expect(notifyPromise).resolves.toEqual({ + key: 'bar/baz', + presence: 'present', + value: 'z', + }); + + expect(bucket.snapshot('bar/baz').value).toEqual('z'); + }); }); 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 3477853241..06ae6fc76a 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -22,7 +22,7 @@ import { import { JsonValue, Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; -const buckets = new Map(); +export const buckets = new Map(); /** * An implementation of the storage API, that uses the browser's local storage. @@ -109,9 +109,16 @@ export class WebStorage implements StorageApi { } private handleStorageChange(eventKey: StorageEvent['key']) { - if (!eventKey?.startsWith(this.namespace)) return; - const key = eventKey?.replace(`${this.namespace}/`, ''); - this.notifyChanges(key); + if (!eventKey?.startsWith(this.namespace)) { + return; + } + // Grab the part of this key that is local to this bucket + const trimmedKey = eventKey?.slice(`${this.namespace}/`.length); + + // If the key still contains a slash, it means it's a sub-bucket + if (!trimmedKey.includes('/')) { + this.notifyChanges(decodeURIComponent(trimmedKey)); + } } private getKeyName(key: string) {