diff --git a/__tests__/html2/hooks/useCapabilities.html b/__tests__/html2/hooks/useCapabilities.html
index 2955275abe..9d5e5e6f61 100644
--- a/__tests__/html2/hooks/useCapabilities.html
+++ b/__tests__/html2/hooks/useCapabilities.html
@@ -52,11 +52,11 @@
expect(initialVoiceConfig).toEqual({ voice: 'en-US', speed: 1.0 });
- // TEST 2: Regular activity should NOT trigger capability re-calculation
+ // TEST 2: Regular activity should NOT trigger capability re-fetch
// Store reference to current voiceConfiguration
const preActivityVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
- // Send a regular message (not capabilitiesChanged event)
+ // Send a regular message (capabilities only update via EventTarget, not activities)
await directLine.emulateIncomingActivity({
type: 'message',
text: 'Hello! This is a regular message.',
@@ -69,13 +69,13 @@
// Get voiceConfiguration after regular activity
const postActivityVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
- // Reference should be the same (no re-calculation for regular activities)
+ // Reference should be the same (activities don't trigger capability re-fetch)
expect(postActivityVoiceConfig).toBe(preActivityVoiceConfig);
- // TEST 3: capabilitiesChanged event SHOULD trigger re-calculation
+ // TEST 3: capabilitieschanged event SHOULD trigger re-fetch
const preChangeVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
- // Update capability and emit event
+ // Update capability and dispatch capabilitieschanged event via EventTarget
directLine.setCapability('getVoiceConfiguration', { voice: 'en-GB', speed: 1.5 }, { emitEvent: true });
// Wait for event to be processed
@@ -92,7 +92,7 @@
// TEST 4: Same value should reuse reference (shallow equality check)
const preNoChangeVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
- // Set same value and emit event
+ // Set same value and dispatch event
directLine.setCapability('getVoiceConfiguration', { voice: 'en-GB', speed: 1.5 }, { emitEvent: true });
// Wait for event to be processed
diff --git a/docs/CAPABILITIES.md b/docs/CAPABILITIES.md
index 99f9d6c60a..af0b672d9f 100644
--- a/docs/CAPABILITIES.md
+++ b/docs/CAPABILITIES.md
@@ -29,16 +29,18 @@ if (voiceConfig) {
## How it works
1. **Initial fetch** - When WebChat mounts, it checks if the adapter exposes capability getter functions and retrieves initial values
-2. **Event-driven updates** - When the adapter emits a `capabilitiesChanged` event, WebChat re-fetches all capabilities from the adapter
+2. **Event-driven updates** - When the adapter dispatches a `capabilitieschanged` event, WebChat re-fetches all capabilities from the adapter
3. **Optimized re-renders** - Only components consuming changed capabilities will re-render
## For adapter implementers
-To expose capabilities from your adapter:
+To expose capabilities from your adapter, implement event listener methods and provide getter functions.
-### 1. Implement getter functions
+### 1. Create an EventTarget and implement getter functions
```js
+const eventTarget = new EventTarget();
+
const adapter = {
// ... other adapter methods
@@ -47,21 +49,19 @@ const adapter = {
sampleRate: 16000,
chunkIntervalMs: 100
};
- }
+ },
+ addEventListener: eventTarget.addEventListener.bind(eventTarget),
+ removeEventListener: eventTarget.removeEventListener.bind(eventTarget)
};
```
-### 2. Emit change events
+### 2. Dispatch change events internally
-When capability values change, emit a `capabilitiesChanged` event activity:
+When capability values change, dispatch a `capabilitieschanged` event using the internal EventTarget:
```js
-// When configuration changes, emit the nudge event
-adapter.activity$.next({
- type: 'event',
- name: 'capabilitiesChanged',
- from: { id: 'bot', role: 'bot' }
-});
+// When configuration changes, dispatch the event internally
+eventTarget.dispatchEvent(new Event('capabilitieschanged'));
```
WebChat will then call all capability getter functions and update consumers if values changed.
diff --git a/packages/api/src/providers/Capabilities/CapabilitiesComposer.tsx b/packages/api/src/providers/Capabilities/CapabilitiesComposer.tsx
index 71b500cf1a..99f34bfcae 100644
--- a/packages/api/src/providers/Capabilities/CapabilitiesComposer.tsx
+++ b/packages/api/src/providers/Capabilities/CapabilitiesComposer.tsx
@@ -1,9 +1,5 @@
-import React, { memo, useCallback, useMemo, type ReactNode } from 'react';
-import { useReduceMemo } from 'use-reduce-memo';
-import type { WebChatActivity } from 'botframework-webchat-core';
-import { literal, object, safeParse } from 'valibot';
+import React, { memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
-import useActivities from '../../hooks/useActivities';
import useWebChatAPIContext from '../../hooks/internal/useWebChatAPIContext';
import CapabilitiesContext from './private/Context';
import fetchCapabilitiesFromAdapter from './private/fetchCapabilitiesFromAdapter';
@@ -13,61 +9,39 @@ type Props = Readonly<{ children?: ReactNode | undefined }>;
const EMPTY_CAPABILITIES: Capabilities = Object.freeze({});
-// Synthetic marker to trigger initial fetch - must be a stable reference
-const INIT_MARKER = Object.freeze({ type: 'capabilities:init' as const });
-type InitMarker = typeof INIT_MARKER;
-type ReducerInput = WebChatActivity | InitMarker;
-
-const CapabilitiesChangedEventSchema = object({
- type: literal('event'),
- name: literal('capabilitiesChanged')
-});
-
-const isInitMarker = (item: ReducerInput): item is InitMarker => item === INIT_MARKER;
-
-const isCapabilitiesChangedEvent = (activity: ReducerInput): boolean =>
- safeParse(CapabilitiesChangedEventSchema, activity).success;
-
/**
- * Composer that derives capabilities from the adapter using a pure derivation pattern.
+ * Composer that provides capabilities from the adapter via EventTarget pattern.
*
* Design principles:
- * 1. Initial fetch: Pulls capabilities from adapter on mount via synthetic init marker
- * 2. Event-driven updates: Re-fetches only when 'capabilitiesChanged' event is detected
+ * 1. Initial fetch: Pulls capabilities from adapter on mount
+ * 2. Event-driven updates: Re-fetches when adapter dispatches 'capabilitieschanged' event
* 3. Stable references: Individual capability objects maintain reference equality if unchanged
* - This ensures consumers using selectors only re-render when their capability changes
*/
const CapabilitiesComposer = memo(({ children }: Props) => {
- const [activities] = useActivities();
const { directLine } = useWebChatAPIContext();
- const activitiesWithInit = useMemo(
- () => Object.freeze([INIT_MARKER, ...activities]),
- [activities]
+ const getAllCapabilities = useCallback(
+ () => fetchCapabilitiesFromAdapter(directLine, EMPTY_CAPABILITIES).capabilities,
+ [directLine]
);
- // TODO: [P1] update to use EventTarget than activity$.
- const capabilities = useReduceMemo(
- activitiesWithInit,
- useCallback(
- (prevCapabilities: Capabilities, item: ReducerInput): Capabilities => {
- const shouldFetch = isInitMarker(item) || isCapabilitiesChangedEvent(item);
+ const [capabilities, setCapabilities] = useState(() => getAllCapabilities());
- if (!shouldFetch) {
- return prevCapabilities;
- }
+ useEffect(() => {
+ const handleCapabilitiesChange = () => {
+ setCapabilities(prevCapabilities => {
+ const { capabilities, hasChanged } = fetchCapabilitiesFromAdapter(directLine, prevCapabilities);
+ return hasChanged ? capabilities : prevCapabilities;
+ });
+ };
- const { capabilities: newCapabilities, hasChanged } = fetchCapabilitiesFromAdapter(
- directLine,
- prevCapabilities
- );
+ if (typeof directLine?.addEventListener === 'function') {
+ directLine.addEventListener('capabilitieschanged', handleCapabilitiesChange);
- return hasChanged ? newCapabilities : prevCapabilities;
- },
- [directLine]
- ),
- EMPTY_CAPABILITIES
- );
+ return () => directLine.removeEventListener('capabilitieschanged', handleCapabilitiesChange);
+ }
+ }, [directLine]);
const contextValue = useMemo(() => Object.freeze({ capabilities }), [capabilities]);
diff --git a/packages/test/page-object/src/globals/testHelpers/createDirectLineEmulator.js b/packages/test/page-object/src/globals/testHelpers/createDirectLineEmulator.js
index e3c902d464..c29e9bdc27 100644
--- a/packages/test/page-object/src/globals/testHelpers/createDirectLineEmulator.js
+++ b/packages/test/page-object/src/globals/testHelpers/createDirectLineEmulator.js
@@ -121,15 +121,12 @@ export default function createDirectLineEmulator({ autoConnect = true, ponyfill
// Generic capabilities storage
const capabilities = new Map();
- // Helper to emit capabilitiesChanged event
+ // EventTarget for capability change notifications
+ const eventTarget = new EventTarget();
+
+ // Helper to dispatch capabilitieschanged event via EventTarget
const emitCapabilitiesChangedEvent = () => {
- activityDeferredObservable.next({
- from: { id: 'bot', role: 'bot' },
- id: uniqueId(),
- name: 'capabilitiesChanged',
- timestamp: getTimestamp(),
- type: 'event'
- });
+ eventTarget.dispatchEvent(new Event('capabilitieschanged'));
};
const directLine = {
@@ -155,6 +152,8 @@ export default function createDirectLineEmulator({ autoConnect = true, ponyfill
emitCapabilitiesChangedEvent();
}
},
+ addEventListener: eventTarget.addEventListener.bind(eventTarget),
+ removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
end: () => {
// This is a mock and will no-op on dispatch().
},