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 = () => {
