You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by en...@apache.org on 2026/05/28 08:06:22 UTC
(superset) 07/13: refactor(extensions): replace per-location pub/sub with registry-wide version counter + useSyncExternalStore
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 <en...@gmail.com>
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 <no...@anthropic.com>
---
.../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 = () => {