From cdfdabb4a9852d922f2e988ef0e958f3a06fb58c Mon Sep 17 00:00:00 2001 From: Henry Su Date: Tue, 15 Sep 2026 21:32:51 -0500 Subject: [PATCH] fix(auth): allow listeners to unsubscribe during notification --- .../src/supabase_auth/_async/gotrue_client.py | 2 +- .../src/supabase_auth/_sync/gotrue_client.py | 2 +- src/auth/tests/_async/test_subscriptions.py | 31 +++++++++++++++++++ src/auth/tests/_sync/test_subscriptions.py | 31 +++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/auth/tests/_async/test_subscriptions.py create mode 100644 src/auth/tests/_sync/test_subscriptions.py diff --git a/src/auth/src/supabase_auth/_async/gotrue_client.py b/src/auth/src/supabase_auth/_async/gotrue_client.py index d7facd8c..4b870558 100644 --- a/src/auth/src/supabase_auth/_async/gotrue_client.py +++ b/src/auth/src/supabase_auth/_async/gotrue_client.py @@ -1139,7 +1139,7 @@ def _notify_all_subscribers( event: AuthChangeEvent, session: Optional[Session], ) -> None: - for subscription in self._state_change_emitters.values(): + for subscription in list(self._state_change_emitters.values()): subscription.callback(event, session) def _get_valid_session( diff --git a/src/auth/src/supabase_auth/_sync/gotrue_client.py b/src/auth/src/supabase_auth/_sync/gotrue_client.py index b3852aa5..117d4985 100644 --- a/src/auth/src/supabase_auth/_sync/gotrue_client.py +++ b/src/auth/src/supabase_auth/_sync/gotrue_client.py @@ -1133,7 +1133,7 @@ def _notify_all_subscribers( event: AuthChangeEvent, session: Optional[Session], ) -> None: - for subscription in self._state_change_emitters.values(): + for subscription in list(self._state_change_emitters.values()): subscription.callback(event, session) def _get_valid_session( diff --git a/src/auth/tests/_async/test_subscriptions.py b/src/auth/tests/_async/test_subscriptions.py new file mode 100644 index 00000000..84ca4dde --- /dev/null +++ b/src/auth/tests/_async/test_subscriptions.py @@ -0,0 +1,31 @@ +from typing import Optional + +from supabase_auth.types import AuthChangeEvent, Session + +from .clients import auth_client + + +async def test_auth_state_change_callback_can_unsubscribe_during_notification() -> None: + client = auth_client() + received: list[tuple[str, AuthChangeEvent]] = [] + + def unsubscribe_on_event( + event: AuthChangeEvent, session: Optional[Session] + ) -> None: + received.append(("once", event)) + subscription.unsubscribe() + + subscription = client.on_auth_state_change(unsubscribe_on_event) + client.on_auth_state_change( + lambda event, session: received.append(("always", event)) + ) + + await client.sign_out() + assert received == [("once", "SIGNED_OUT"), ("always", "SIGNED_OUT")] + + await client.sign_out() + assert received == [ + ("once", "SIGNED_OUT"), + ("always", "SIGNED_OUT"), + ("always", "SIGNED_OUT"), + ] diff --git a/src/auth/tests/_sync/test_subscriptions.py b/src/auth/tests/_sync/test_subscriptions.py new file mode 100644 index 00000000..a3cef5c1 --- /dev/null +++ b/src/auth/tests/_sync/test_subscriptions.py @@ -0,0 +1,31 @@ +from typing import Optional + +from supabase_auth.types import AuthChangeEvent, Session + +from .clients import auth_client + + +def test_auth_state_change_callback_can_unsubscribe_during_notification() -> None: + client = auth_client() + received: list[tuple[str, AuthChangeEvent]] = [] + + def unsubscribe_on_event( + event: AuthChangeEvent, session: Optional[Session] + ) -> None: + received.append(("once", event)) + subscription.unsubscribe() + + subscription = client.on_auth_state_change(unsubscribe_on_event) + client.on_auth_state_change( + lambda event, session: received.append(("always", event)) + ) + + client.sign_out() + assert received == [("once", "SIGNED_OUT"), ("always", "SIGNED_OUT")] + + client.sign_out() + assert received == [ + ("once", "SIGNED_OUT"), + ("always", "SIGNED_OUT"), + ("always", "SIGNED_OUT"), + ]