This is an automated email from the ASF dual-hosted git repository.

EnxDev pushed a commit to branch test/chatbot-local
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 504826bb24d9eeb8bce34d5a9db14af44fa58b84
Author: Enzo Martellucci <[email protected]>
AuthorDate: Wed May 27 22:52:57 2026 +0200

    refactor(extensions): replace per-location pub/sub with registry-wide 
version counter + useSyncExternalStore
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../src/components/ChatbotMount/index.tsx          | 30 +++++++++---------
 superset-frontend/src/core/views/index.ts          | 37 +++++++++++-----------
 .../src/extensions/ExtensionsList.tsx              | 21 ++++++------
 3 files changed, 44 insertions(+), 44 deletions(-)

diff --git a/superset-frontend/src/components/ChatbotMount/index.tsx 
b/superset-frontend/src/components/ChatbotMount/index.tsx
index 87d4894a918..116e6743bfd 100644
--- a/superset-frontend/src/components/ChatbotMount/index.tsx
+++ b/superset-frontend/src/components/ChatbotMount/index.tsx
@@ -16,15 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useState, useEffect, useCallback } from 'react';
+import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } 
from 'react';
 import { ReactElement } from 'react';
 import { SupersetClient } from '@superset-ui/core';
 import { css, useTheme } from '@apache-superset/core/theme';
 import { ErrorBoundary } from 'src/components/ErrorBoundary';
 import { getActiveChatbot } from 'src/core/chatbot';
-import { subscribeToLocation } from 'src/core/views';
+import { subscribeToRegistry, getRegistryVersion } from 'src/core/views';
 import { subscribeToExtensionSettings } from 'src/core/extensions';
-import { CHATBOT_LOCATION } from 'src/views/contributions';
 
 const CHATBOT_EDGE_MARGIN = 24;
 
@@ -36,12 +35,9 @@ const ChatbotRenderer = ({ provider }: { provider: () => 
ReactElement }) =>
 
 const ChatbotMount = () => {
   const theme = useTheme();
-  const [adminSelectedId, setAdminSelectedId] = useState<string | null>(null);
-  const [enabledMap, setEnabledMap] = useState<Record<string, boolean>>({});
   // null = settings not yet loaded; don't render anything until settings 
arrive.
-  const [activeChatbot, setActiveChatbot] = useState<ActiveChatbot | null>(
-    null,
-  );
+  const [adminSelectedId, setAdminSelectedId] = useState<string | null | 
undefined>(undefined);
+  const [enabledMap, setEnabledMap] = useState<Record<string, boolean>>({});
 
   const fetchSettings = useCallback(() => {
     let cancelled = false;
@@ -52,11 +48,10 @@ const ChatbotMount = () => {
         const enabled: Record<string, boolean> = json.result?.enabled ?? {};
         setAdminSelectedId(id);
         setEnabledMap(enabled);
-        setActiveChatbot(getActiveChatbot(id, enabled));
       })
       .catch(() => {
         // Settings fetch failure is non-fatal — fall back to 
first-to-register.
-        setActiveChatbot(getActiveChatbot(null, {}));
+        setAdminSelectedId(null);
       });
     return () => {
       cancelled = true;
@@ -67,12 +62,17 @@ const ChatbotMount = () => {
 
   useEffect(() => subscribeToExtensionSettings(fetchSettings), 
[fetchSettings]);
 
-  useEffect(
+  const registryVersion = useSyncExternalStore(
+    subscribeToRegistry,
+    getRegistryVersion,
+  );
+
+  const activeChatbot = useMemo(
     () =>
-      subscribeToLocation(CHATBOT_LOCATION, () =>
-        setActiveChatbot(getActiveChatbot(adminSelectedId, enabledMap)),
-      ),
-    [adminSelectedId, enabledMap],
+      adminSelectedId === undefined
+        ? null
+        : getActiveChatbot(adminSelectedId, enabledMap),
+    [adminSelectedId, enabledMap, registryVersion],
   );
 
   if (!activeChatbot) {
diff --git a/superset-frontend/src/core/views/index.ts 
b/superset-frontend/src/core/views/index.ts
index 3806bb19979..bc3bea3588f 100644
--- a/superset-frontend/src/core/views/index.ts
+++ b/superset-frontend/src/core/views/index.ts
@@ -39,27 +39,27 @@ const viewRegistry: Map<
 
 const locationIndex: Map<string, Set<string>> = new Map();
 
-/** Listeners notified whenever a view is registered or unregistered at a 
location. */
-const locationListeners: Map<string, Set<() => void>> = new Map();
+/**
+ * Monotonic version of the view registry. Bumped on every registration or
+ * disposal so consumers can re-derive state via React's 
`useSyncExternalStore`.
+ */
+let registryVersion = 0;
+const registrySubscribers = new Set<() => void>();
 
-const notifyListeners = (location: string) => {
-  locationListeners.get(location)?.forEach(fn => fn());
+const notifyRegistry = () => {
+  registryVersion += 1;
+  registrySubscribers.forEach(fn => fn());
 };
 
-/**
- * Subscribe to registration changes at a specific location.
- * Returns an unsubscribe function.
- */
-export const subscribeToLocation = (
-  location: string,
-  listener: () => void,
-): (() => void) => {
-  const listeners = locationListeners.get(location) ?? new Set();
-  listeners.add(listener);
-  locationListeners.set(location, listeners);
-  return () => listeners.delete(listener);
+export const subscribeToRegistry = (listener: () => void): (() => void) => {
+  registrySubscribers.add(listener);
+  return () => {
+    registrySubscribers.delete(listener);
+  };
 };
 
+export const getRegistryVersion = () => registryVersion;
+
 const registerView: typeof viewsApi.registerView = (
   view: View,
   location: string,
@@ -70,7 +70,6 @@ const registerView: typeof viewsApi.registerView = (
   const previousLocation = viewRegistry.get(id)?.location;
   if (previousLocation && previousLocation !== location) {
     locationIndex.get(previousLocation)?.delete(id);
-    notifyListeners(previousLocation);
   }
 
   viewRegistry.set(id, { view, location, provider });
@@ -79,13 +78,13 @@ const registerView: typeof viewsApi.registerView = (
   ids.add(id);
   locationIndex.set(location, ids);
 
-  notifyListeners(location);
+  notifyRegistry();
 
   return new Disposable(() => {
     const registeredLocation = viewRegistry.get(id)?.location ?? location;
     viewRegistry.delete(id);
     locationIndex.get(registeredLocation)?.delete(id);
-    notifyListeners(registeredLocation);
+    notifyRegistry();
   });
 };
 
diff --git a/superset-frontend/src/extensions/ExtensionsList.tsx 
b/superset-frontend/src/extensions/ExtensionsList.tsx
index f57cefba970..9e8df4c43f8 100644
--- a/superset-frontend/src/extensions/ExtensionsList.tsx
+++ b/superset-frontend/src/extensions/ExtensionsList.tsx
@@ -24,6 +24,7 @@ import {
   useMemo,
   useRef,
   useState,
+  useSyncExternalStore,
 } from 'react';
 import { SupersetClient } from '@superset-ui/core';
 import { ConfirmStatusChange, Tooltip } from '@superset-ui/core/components';
@@ -35,7 +36,11 @@ import { ListView } from 'src/components';
 import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import { CHATBOT_LOCATION } from 'src/views/contributions';
-import { getRegisteredViewIds, subscribeToLocation } from 'src/core/views';
+import {
+  getRegisteredViewIds,
+  subscribeToRegistry,
+  getRegistryVersion,
+} from 'src/core/views';
 import { notifyExtensionSettingsChanged } from 'src/core/extensions';
 
 const PAGE_SIZE = 25;
@@ -84,13 +89,9 @@ const ExtensionsList: FunctionComponent<ExtensionsListProps> 
= ({
   const settingsRef = useRef(settings);
   settingsRef.current = settings;
 
-  const [chatbotRegistryVersion, setChatbotRegistryVersion] = useState(0);
-  useEffect(
-    () =>
-      subscribeToLocation(CHATBOT_LOCATION, () =>
-        setChatbotRegistryVersion(v => v + 1),
-      ),
-    [],
+  const registryVersion = useSyncExternalStore(
+    subscribeToRegistry,
+    getRegistryVersion,
   );
 
   useEffect(() => {
@@ -139,9 +140,9 @@ const ExtensionsList: 
FunctionComponent<ExtensionsListProps> = ({
 
   const chatbotIds = useMemo(
     () => new Set(getRegisteredViewIds(CHATBOT_LOCATION)),
-    // chatbotRegistryVersion is intentionally in deps to re-evaluate when 
views register
+    // registryVersion is intentionally in deps to re-evaluate when views 
register
     // eslint-disable-next-line react-hooks/exhaustive-deps
-    [chatbotRegistryVersion],
+    [registryVersion],
   );
 
   const handleUploadClick = () => {

Reply via email to