From 96234ce3d44ec6f262c07cc7416171f4cb82e07b Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 23 Aug 2024 10:26:15 -0700 Subject: [PATCH] fix(clerk-react): Handle multiple `addListener` calls (#4010) --- .changeset/itchy-tigers-own.md | 5 ++ .../src/__tests__/isomorphicClerk.test.ts | 51 +++++++++++++++++++ packages/react/src/isomorphicClerk.ts | 26 ++++++++-- 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 .changeset/itchy-tigers-own.md diff --git a/.changeset/itchy-tigers-own.md b/.changeset/itchy-tigers-own.md new file mode 100644 index 0000000000..aec468b9d8 --- /dev/null +++ b/.changeset/itchy-tigers-own.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-react": patch +--- + +Fix multiple `addListener` method calls diff --git a/packages/react/src/__tests__/isomorphicClerk.test.ts b/packages/react/src/__tests__/isomorphicClerk.test.ts index 0b4618ebbb..90f17bd441 100644 --- a/packages/react/src/__tests__/isomorphicClerk.test.ts +++ b/packages/react/src/__tests__/isomorphicClerk.test.ts @@ -1,3 +1,5 @@ +import type { Resources, UnsubscribeCallback } from '@clerk/types'; + import { IsomorphicClerk } from '../isomorphicClerk'; describe('isomorphicClerk', () => { @@ -49,4 +51,53 @@ describe('isomorphicClerk', () => { { appearance: { baseTheme: 'white' } }, ]); }); + + it('handles multiple resource listeners', async () => { + const listenerCallHistory: Array = []; + const addedListeners: Map<(payload: Resources) => void, { unsubscribe: UnsubscribeCallback }> = new Map(); + + const dummyClerkJS = { + addListener: (listener: (payload: Resources) => void) => { + const unsubscribe = () => { + addedListeners.delete(listener); + }; + addedListeners.set(listener, { unsubscribe }); + return unsubscribe; + }, + }; + + const isomorphicClerk = new IsomorphicClerk({ publishableKey: 'pk_test_xxx' }); + (isomorphicClerk as any).clerkjs = dummyClerkJS as any; + + const unsubscribe1 = isomorphicClerk.addListener(payload => listenerCallHistory.push(payload)); + const unsubscribe2 = isomorphicClerk.addListener(payload => listenerCallHistory.push(payload)); + + // Unsubscribe one listener before ClerkJS is loaded + unsubscribe1(); + + jest.spyOn(isomorphicClerk, 'loaded', 'get').mockReturnValue(true); + isomorphicClerk.emitLoaded(); + const unsubscribe3 = isomorphicClerk.addListener(payload => listenerCallHistory.push(payload)); + + // Simulate ClerkJS triggering the listeners + const mockPayload = { + user: { id: 'user_xxx' }, + session: { id: 'sess_xxx' }, + client: { id: 'client_xxx' }, + organization: undefined, + } as Resources; + addedListeners.forEach((_, listener) => listener(mockPayload)); + + expect(listenerCallHistory).toEqual([mockPayload, mockPayload]); + expect(listenerCallHistory.length).toBe(2); + + // Unsubscribe all remaining listeners + unsubscribe2(); + unsubscribe3(); + listenerCallHistory.length = 0; + addedListeners.forEach((_, listener) => listener(mockPayload)); + + expect(listenerCallHistory).toEqual([]); + expect(listenerCallHistory.length).toBe(0); + }); }); diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 4b21a188f2..9c9fad481b 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -168,6 +168,14 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountOrganizationSwitcherNodes = new Map(); private premountOrganizationListNodes = new Map(); private premountMethodCalls = new Map, MethodCallback>(); + // A separate Map of `addListener` method calls to handle multiple listeners. + private premountAddListenerCalls = new Map< + ListenerCallback, + { + unsubscribe: UnsubscribeCallback; + nativeUnsubscribe?: UnsubscribeCallback; + } + >(); private loadedListeners: Array<() => void> = []; #loaded = false; @@ -477,6 +485,9 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.clerkjs = clerkjs; this.premountMethodCalls.forEach(cb => cb()); + this.premountAddListenerCalls.forEach((listenerHandlers, listener) => { + listenerHandlers.nativeUnsubscribe = clerkjs.addListener(listener); + }); if (this.preopenSignIn !== null) { clerkjs.openSignIn(this.preopenSignIn); @@ -834,13 +845,18 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }; addListener = (listener: ListenerCallback): UnsubscribeCallback => { - const callback = () => this.clerkjs?.addListener(listener); - if (this.clerkjs) { - return callback() as UnsubscribeCallback; + return this.clerkjs.addListener(listener); } else { - this.premountMethodCalls.set('addListener', callback); - return () => this.premountMethodCalls.delete('addListener'); + const unsubscribe = () => { + const listenerHandlers = this.premountAddListenerCalls.get(listener); + if (listenerHandlers) { + listenerHandlers.nativeUnsubscribe?.(); + this.premountAddListenerCalls.delete(listener); + } + }; + this.premountAddListenerCalls.set(listener, { unsubscribe, nativeUnsubscribe: undefined }); + return unsubscribe; } };