diff --git a/packages/auth/__tests__/providers/cognito/tokenProvider/tokenStore.test.ts b/packages/auth/__tests__/providers/cognito/tokenProvider/tokenStore.test.ts index 9338cee86e8..f9da3016409 100644 --- a/packages/auth/__tests__/providers/cognito/tokenProvider/tokenStore.test.ts +++ b/packages/auth/__tests__/providers/cognito/tokenProvider/tokenStore.test.ts @@ -1,10 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { KeyValueStorageInterface } from '@aws-amplify/core'; -import { decodeJWT } from '@aws-amplify/core/internals/utils'; +import { Hub, KeyValueStorageInterface } from '@aws-amplify/core'; +import { AMPLIFY_SYMBOL, decodeJWT } from '@aws-amplify/core/internals/utils'; -import { DefaultTokenStore } from '../../../../src/providers/cognito/tokenProvider'; +import { + AUTH_KEY_PREFIX, + DefaultTokenStore, +} from '../../../../src/providers/cognito/tokenProvider'; const userPoolId = 'us-west-1:0000523'; const userPoolClientId = 'mockCognitoUserPoolsId'; @@ -21,6 +24,9 @@ jest.mock( TokenProviderErrorCode: 'mockErrorCode', }), ); +jest.mock('../../../../src/providers/cognito/apis/getCurrentUser', () => ({ + getCurrentUser: () => Promise.resolve({}), +})); const mockedDecodeJWT = jest.mocked(decodeJWT); mockedDecodeJWT.mockReturnValue({ @@ -72,6 +78,7 @@ const mockKeyValueStorage: jest.Mocked = { getItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), + addListener: jest.fn(), }; describe('TokenStore', () => { @@ -403,4 +410,83 @@ describe('TokenStore', () => { expect(finalTokens?.refreshToken).toBe(newMockAuthToken.refreshToken); }); }); + + describe('setupNotify', () => { + it('should setup a KeyValueStorageEvent listener', async () => { + tokenStore.setupNotify(); + + const spy = jest.spyOn(keyValStorage, 'addListener'); + const hubSpy = jest.spyOn(Hub, 'dispatch'); + + expect(spy).toHaveBeenCalledWith(expect.any(Function)); + + const listener = spy.mock.calls[0][0]; + + // does nothing if key does not match + await listener({ + key: 'foo.bar', + oldValue: null, + newValue: null, + }); + + expect(hubSpy).not.toHaveBeenCalled(); + + // does nothing if both values are null + await listener({ + key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`, + oldValue: null, + newValue: null, + }); + + expect(hubSpy).not.toHaveBeenCalled(); + + // dispatches signedIn on new value + await listener({ + key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`, + newValue: '123', + oldValue: null, + }); + + expect(hubSpy).toHaveBeenCalledWith( + 'auth', + { event: 'signedIn', data: {} }, + 'Auth', + AMPLIFY_SYMBOL, + true, + ); + hubSpy.mockClear(); + + // dispatches signedOut on null newValue + await listener({ + key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`, + newValue: null, + oldValue: '123', + }); + + expect(hubSpy).toHaveBeenCalledWith( + 'auth', + { event: 'signedOut' }, + 'Auth', + AMPLIFY_SYMBOL, + true, + ); + hubSpy.mockClear(); + + // dispatches tokenRefresh for changed value + await listener({ + key: `${AUTH_KEY_PREFIX}.someid.someotherId.refreshToken`, + newValue: '456', + oldValue: '123', + }); + + expect(hubSpy).toHaveBeenCalledWith( + 'auth', + { event: 'tokenRefresh' }, + 'Auth', + AMPLIFY_SYMBOL, + true, + ); + hubSpy.mockClear(); + }); + }); }); diff --git a/packages/auth/src/providers/cognito/tokenProvider/CognitoUserPoolsTokenProvider.ts b/packages/auth/src/providers/cognito/tokenProvider/CognitoUserPoolsTokenProvider.ts index 43f0f8a2d8c..98036f552dd 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/CognitoUserPoolsTokenProvider.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/CognitoUserPoolsTokenProvider.ts @@ -23,6 +23,7 @@ export class CognitoUserPoolsTokenProvider constructor() { this.authTokenStore = new DefaultTokenStore(); this.authTokenStore.setKeyValueStorage(defaultStorage); + this.authTokenStore.setupNotify(); this.tokenOrchestrator = new TokenOrchestrator(); this.tokenOrchestrator.setAuthTokenStore(this.authTokenStore); this.tokenOrchestrator.setTokenRefresher(refreshAuthTokens); diff --git a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts index dae3ec26ff7..41a04a8f224 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts @@ -1,12 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AuthConfig, KeyValueStorageInterface } from '@aws-amplify/core'; +import { AuthConfig, Hub, KeyValueStorageInterface } from '@aws-amplify/core'; import { + AMPLIFY_SYMBOL, assertTokenProviderConfig, decodeJWT, } from '@aws-amplify/core/internals/utils'; +import { KeyValueStorageEvent } from '@aws-amplify/core/src/types'; import { AuthError } from '../../../errors/AuthError'; +import { getCurrentUser } from '../apis/getCurrentUser'; import { AuthKeys, @@ -42,6 +45,47 @@ export class DefaultTokenStore implements AuthTokenStore { this.authConfig = authConfig; } + setupNotify() { + this.keyValueStorage?.addListener?.(async (e: KeyValueStorageEvent) => { + const [key, , , id] = (e.key || '').split('.'); + if (key === AUTH_KEY_PREFIX && id === 'refreshToken') { + const { newValue, oldValue } = e; + if (newValue && oldValue === null) { + Hub.dispatch( + 'auth', + { + event: 'signedIn', + data: await getCurrentUser(), + }, + 'Auth', + AMPLIFY_SYMBOL, + true, + ); + } else if (newValue === null && oldValue) { + Hub.dispatch( + 'auth', + { + event: 'signedOut', + }, + 'Auth', + AMPLIFY_SYMBOL, + true, + ); + } else if (newValue && oldValue) { + Hub.dispatch( + 'auth', + { + event: 'tokenRefresh', + }, + 'Auth', + AMPLIFY_SYMBOL, + true, + ); + } + } + }); + } + async loadTokens(): Promise { // TODO(v6): migration logic should be here // Reading V5 tokens old format diff --git a/packages/auth/src/providers/cognito/utils/signInWithRedirectStore.ts b/packages/auth/src/providers/cognito/utils/signInWithRedirectStore.ts index a72469b6ab5..603d971fd37 100644 --- a/packages/auth/src/providers/cognito/utils/signInWithRedirectStore.ts +++ b/packages/auth/src/providers/cognito/utils/signInWithRedirectStore.ts @@ -7,13 +7,13 @@ import { } from '@aws-amplify/core'; import { assertTokenProviderConfig } from '@aws-amplify/core/internals/utils'; +import { AUTH_KEY_PREFIX } from '../tokenProvider/constants'; import { getAuthStorageKeys } from '../tokenProvider/TokenStore'; import { OAuthStorageKeys, OAuthStore } from './types'; const V5_HOSTED_UI_KEY = 'amplify-signin-with-hostedUI'; -const name = 'CognitoIdentityServiceProvider'; export class DefaultOAuthStore implements OAuthStore { keyValueStorage: KeyValueStorageInterface; cognitoConfig?: CognitoUserPoolConfig; @@ -26,7 +26,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); await Promise.all([ @@ -39,7 +39,7 @@ export class DefaultOAuthStore implements OAuthStore { async clearOAuthData(): Promise { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); await this.clearOAuthInflightData(); @@ -52,7 +52,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); @@ -63,7 +63,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); @@ -74,7 +74,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); @@ -85,7 +85,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); @@ -100,7 +100,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); @@ -112,7 +112,7 @@ export class DefaultOAuthStore implements OAuthStore { async storeOAuthInFlight(inflight: boolean): Promise { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); @@ -126,7 +126,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); @@ -151,7 +151,7 @@ export class DefaultOAuthStore implements OAuthStore { assertTokenProviderConfig(this.cognitoConfig); const authKeys = createKeysForAuthStorage( - name, + AUTH_KEY_PREFIX, this.cognitoConfig.userPoolClientId, ); diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 01c322bc751..f3ff0788c5c 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -307,7 +307,7 @@ "name": "[Analytics] record (Pinpoint)", "path": "./dist/esm/analytics/index.mjs", "import": "{ record }", - "limit": "18.10 kB" + "limit": "18.17 kB" }, { "name": "[Analytics] record (Kinesis)", @@ -427,19 +427,19 @@ "name": "[Auth] setUpTOTP (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ setUpTOTP }", - "limit": "14 kB" + "limit": "14.08 kB" }, { "name": "[Auth] updateUserAttributes (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ updateUserAttributes }", - "limit": "13 kB" + "limit": "13.01 kB" }, { "name": "[Auth] getCurrentUser (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ getCurrentUser }", - "limit": "8.30 kB" + "limit": "8.37 kB" }, { "name": "[Auth] confirmUserAttribute (Cognito)", @@ -493,7 +493,7 @@ "name": "[Storage] copy (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ copy }", - "limit": "17 kB" + "limit": "17.08 kB" }, { "name": "[Storage] downloadData (S3)", diff --git a/packages/core/__tests__/Hub.test.ts b/packages/core/__tests__/Hub.test.ts index e7cd42a8f4f..2ed23450887 100644 --- a/packages/core/__tests__/Hub.test.ts +++ b/packages/core/__tests__/Hub.test.ts @@ -140,4 +140,76 @@ describe('Hub', () => { expect(loggerSpy).not.toHaveBeenCalled(); }); }); + + describe('crossTab', () => { + const crossTabListener = jest.fn(); + const uiCrossTabListener = jest.fn(); + const sameTabListener = jest.fn(); + beforeAll(() => { + Hub.listen('auth', crossTabListener, { + enableCrossTabEvents: true, + }); + Hub.listen('ui' as 'auth', uiCrossTabListener, { + enableCrossTabEvents: true, + }); + Hub.listen('auth', sameTabListener); + }); + + beforeEach(() => { + crossTabListener.mockClear(); + sameTabListener.mockClear(); + }); + + it('should not call crossTab listeners on sameTab events', () => { + Hub.dispatch( + 'auth', + { + event: 'signedIn', + data: {}, + }, + 'Auth', + Symbol.for('amplify_default'), + ); + + expect(crossTabListener).not.toHaveBeenCalled(); + expect(sameTabListener).toHaveBeenCalled(); + }); + + it('should call crossTab listeners on crossTab events', () => { + Hub.dispatch( + 'auth', + { + event: 'signedIn', + data: { + username: 'foo', + userId: '123', + }, + }, + 'Auth', + Symbol.for('amplify_default'), + true, + ); + + expect(crossTabListener).toHaveBeenCalled(); + expect(sameTabListener).not.toHaveBeenCalled(); + }); + + it('should not allow crossTab dispatch in other channels', () => { + Hub.dispatch( + // this looks weird but is only used to mute TS. + // becase the API can be called this way. + // and we want to check the logic, not the types + 'ui' as 'auth', + { + event: 'tokenRefresh', + message: 'whooza', + }, + 'Auth', + Symbol.for('amplify_default'), + true, + ); + + expect(uiCrossTabListener).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/core/__tests__/storage/DefaultStorage.test.ts b/packages/core/__tests__/storage/DefaultStorage.test.ts index 519adb01af9..6be1df5e0b5 100644 --- a/packages/core/__tests__/storage/DefaultStorage.test.ts +++ b/packages/core/__tests__/storage/DefaultStorage.test.ts @@ -1,5 +1,6 @@ import { DefaultStorage } from '../../src/storage/DefaultStorage'; import { InMemoryStorage } from '../../src/storage/InMemoryStorage'; +import * as utils from '../../src/utils'; const key = 'k'; const value = 'value'; @@ -57,4 +58,16 @@ describe('DefaultStorage', () => { value: originalLocalStorage, }); }); + + it('should setup listeners, when in browser', () => { + jest.spyOn(utils, 'isBrowser').mockImplementation(() => true); + const windowSpy = jest.spyOn(window, 'addEventListener'); + + defaultStorage = new DefaultStorage(); + expect(windowSpy).toHaveBeenCalledWith( + 'storage', + expect.any(Function), + false, + ); + }); }); diff --git a/packages/core/__tests__/storage/SessionStorage.test.ts b/packages/core/__tests__/storage/SessionStorage.test.ts index 81545838f12..f6591447a2a 100644 --- a/packages/core/__tests__/storage/SessionStorage.test.ts +++ b/packages/core/__tests__/storage/SessionStorage.test.ts @@ -1,5 +1,6 @@ import { InMemoryStorage } from '../../src/storage/InMemoryStorage'; import { SessionStorage } from '../../src/storage/SessionStorage'; +import * as utils from '../../src/utils'; const key = 'k'; const value = 'value'; @@ -59,4 +60,16 @@ describe('SessionStorage', () => { value: originalSessionStorage, }); }); + + it('should setup listeners, when in browser', () => { + jest.spyOn(utils, 'isBrowser').mockImplementation(() => true); + const windowSpy = jest.spyOn(window, 'addEventListener'); + + sessionStorage = new SessionStorage(); + expect(windowSpy).toHaveBeenCalledWith( + 'storage', + expect.any(Function), + false, + ); + }); }); diff --git a/packages/core/package.json b/packages/core/package.json index 5f44cb85515..9fbea46be45 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -69,7 +69,7 @@ "name": "Core (Hub)", "path": "./dist/esm/index.mjs", "import": "{ Hub }", - "limit": "1.46 kB" + "limit": "1.52 kB" }, { "name": "Core (I18n)", @@ -81,7 +81,7 @@ "name": "Custom clients (fetch handler)", "path": "./dist/esm/clients/handlers/fetch.mjs", "import": "{ fetchTransferHandler }", - "limit": "900 B" + "limit": "700 B" }, { "name": "Custom clients (unauthenticated handler)", @@ -105,7 +105,7 @@ "name": "Cache (default browser storage)", "path": "./dist/esm/index.mjs", "import": "{ Cache }", - "limit": "3.4 kB" + "limit": "3.41 kB" } ], "exports": { diff --git a/packages/core/src/Hub/index.ts b/packages/core/src/Hub/index.ts index b82b7040b87..3b92c74aecc 100644 --- a/packages/core/src/Hub/index.ts +++ b/packages/core/src/Hub/index.ts @@ -82,6 +82,14 @@ export class HubClass { ampSymbol?: symbol, ): void; + dispatch( + channel: Channel, + payload: HubPayload, + source?: string, + ampSymbol?: symbol, + crossTab?: boolean, + ): void; + dispatch( channel: string, payload: HubPayload, @@ -97,6 +105,7 @@ export class HubClass { payload: HubPayload, source?: string, ampSymbol?: symbol, + crossTab?: boolean, ): void { if ( typeof channel === 'string' && @@ -113,6 +122,7 @@ export class HubClass { const capsule: HubCapsule = { channel, + crossTab, payload: { ...payload }, source, patternInfo: [], @@ -140,6 +150,16 @@ export class HubClass { listenerName?: string, ): StopListenerCallback; + // the crosstab option is only available for the 'auth' channel + listen( + channel: Channel, + callback: HubCallback, + options?: { + listenerName?: string; + enableCrossTabEvents?: boolean; + }, + ): StopListenerCallback; + listen( channel: string, callback: HubCallback, @@ -152,9 +172,19 @@ export class HubClass { >( channel: Channel, callback: HubCallback, - listenerName = 'noname', + options: + | string + | { listenerName?: string; enableCrossTabEvents?: boolean } = { + listenerName: 'noname', + }, ): StopListenerCallback { - let cb: HubCallback; + let cb: HubCallback; + let o; + if (typeof options === 'string') { + o = { listenerName: options, enableCrossTabEvents: false }; + } else { + o = { listenerName: 'noname', enableCrossTabEvents: false, ...options }; + } if (typeof callback !== 'function') { throw new AmplifyError({ name: NO_HUBCALLBACK_PROVIDED_EXCEPTION, @@ -162,7 +192,7 @@ export class HubClass { }); } else { // Needs to be casted as a more generic type - cb = callback as HubCallback; + cb = callback as HubCallback; } let holder = this.listeners.get(channel); @@ -172,7 +202,10 @@ export class HubClass { } holder.push({ - name: listenerName, + name: (o as { listenerName: string; enableCrossTabEvents: boolean }) + .listenerName, + crossTab: (o as { listenerName: string; enableCrossTabEvents: boolean }) + .enableCrossTabEvents, callback: cb, }); @@ -184,10 +217,16 @@ export class HubClass { private _toListeners( capsule: HubCapsule, ) { - const { channel, payload } = capsule; + const { channel, payload, crossTab } = capsule; const holder = this.listeners.get(channel); if (holder) { - holder.forEach(listener => { + const crossTabHolders = holder.filter(listener => { + const sameCrossTabSpec = !!crossTab === !!listener.crossTab; + const rightChannel = crossTab ? channel === 'auth' : true; + + return sameCrossTabSpec && rightChannel; + }); + crossTabHolders.forEach(listener => { logger.debug(`Dispatching to ${channel} with `, payload); try { listener.callback(capsule); diff --git a/packages/core/src/Hub/types/HubTypes.ts b/packages/core/src/Hub/types/HubTypes.ts index 3a766eb9389..d5aa6b1e9cc 100644 --- a/packages/core/src/Hub/types/HubTypes.ts +++ b/packages/core/src/Hub/types/HubTypes.ts @@ -8,6 +8,7 @@ export type IListener< EventData extends EventDataMap = EventDataMap, > = { name: string; + crossTab: boolean; callback: HubCallback; }[]; @@ -31,6 +32,7 @@ export interface HubCapsule< channel: Channel; payload: HubPayload; source?: string; + crossTab?: boolean; patternInfo?: string[]; } diff --git a/packages/core/src/storage/DefaultStorage.ts b/packages/core/src/storage/DefaultStorage.ts index fdacd0e6550..5d740071461 100644 --- a/packages/core/src/storage/DefaultStorage.ts +++ b/packages/core/src/storage/DefaultStorage.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { isBrowser } from '../utils'; + import { KeyValueStorage } from './KeyValueStorage'; import { getLocalStorageWithFallback } from './utils'; @@ -10,5 +12,21 @@ import { getLocalStorageWithFallback } from './utils'; export class DefaultStorage extends KeyValueStorage { constructor() { super(getLocalStorageWithFallback()); + this.storageListener = this.storageListener.bind(this); + + if (isBrowser()) { + window.addEventListener('storage', this.storageListener, false); + this.listeners = new Set(); + } } + + private storageListener = (e: StorageEvent) => { + this.listeners?.forEach(listener => { + listener({ + key: e.key, + oldValue: e.oldValue, + newValue: e.newValue, + }); + }); + }; } diff --git a/packages/core/src/storage/KeyValueStorage.ts b/packages/core/src/storage/KeyValueStorage.ts index 0307445f028..7bc67fdc1bc 100644 --- a/packages/core/src/storage/KeyValueStorage.ts +++ b/packages/core/src/storage/KeyValueStorage.ts @@ -2,13 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 import { PlatformNotSupportedError } from '../errors'; -import { KeyValueStorageInterface } from '../types'; +import { KeyValueStorageEvent, KeyValueStorageInterface } from '../types'; /** * @internal */ export class KeyValueStorage implements KeyValueStorageInterface { storage?: Storage; + listeners?: Set<(e: KeyValueStorageEvent) => void>; constructor(storage?: Storage) { this.storage = storage; @@ -55,4 +56,22 @@ export class KeyValueStorage implements KeyValueStorageInterface { if (!this.storage) throw new PlatformNotSupportedError(); this.storage.clear(); } + + /** + * This is used to allow listening for changes + * @param {function} listener - the function called on storage change + */ + addListener(listener: (ev: KeyValueStorageEvent) => Promise) { + if (!this.listeners) { + return; + } + this.listeners.add(listener); + } + + rmListener(listener: (ev: KeyValueStorageEvent) => Promise) { + if (!this.listeners) { + return; + } + this.listeners.delete(listener); + } } diff --git a/packages/core/src/storage/SessionStorage.ts b/packages/core/src/storage/SessionStorage.ts index c8866b78d7a..beb2849fdc3 100644 --- a/packages/core/src/storage/SessionStorage.ts +++ b/packages/core/src/storage/SessionStorage.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { isBrowser } from '../utils'; + import { KeyValueStorage } from './KeyValueStorage'; import { getSessionStorageWithFallback } from './utils'; @@ -10,5 +12,21 @@ import { getSessionStorageWithFallback } from './utils'; export class SessionStorage extends KeyValueStorage { constructor() { super(getSessionStorageWithFallback()); + this.storageListener = this.storageListener.bind(this); + + if (isBrowser()) { + window.addEventListener('storage', this.storageListener, false); + this.listeners = new Set(); + } } + + private storageListener = (e: StorageEvent) => { + this.listeners?.forEach(listener => { + listener({ + key: e.key, + oldValue: e.oldValue, + newValue: e.newValue, + }); + }); + }; } diff --git a/packages/core/src/types/storage.ts b/packages/core/src/types/storage.ts index 79a0c63e74b..efe0095d2b6 100644 --- a/packages/core/src/types/storage.ts +++ b/packages/core/src/types/storage.ts @@ -6,6 +6,8 @@ export interface KeyValueStorageInterface { getItem(key: string): Promise; removeItem(key: string): Promise; clear(): Promise; + addListener?(listener: (ev: KeyValueStorageEvent) => Promise): void; + rmListener?(listener: (ev: KeyValueStorageEvent) => Promise): void; } export type SameSite = 'strict' | 'lax' | 'none'; @@ -28,3 +30,9 @@ export interface SyncStorage { removeItem(key: string): void; clear(): void; } + +export interface KeyValueStorageEvent { + readonly key: string | null; + readonly oldValue: any; + readonly newValue: any; +}