Skip to content

Commit 118af24

Browse files
Tech debt: Capabilities using event target (#5687)
* capabilities using event target * Cleaned up * Fix Prettier * comment resolved --------- Co-authored-by: William Wong <compulim@users.noreply.github.com>
1 parent 32ae93a commit 118af24

File tree

4 files changed

+45
-72
lines changed

4 files changed

+45
-72
lines changed

__tests__/html2/hooks/useCapabilities.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@
5252

5353
expect(initialVoiceConfig).toEqual({ voice: 'en-US', speed: 1.0 });
5454

55-
// TEST 2: Regular activity should NOT trigger capability re-calculation
55+
// TEST 2: Regular activity should NOT trigger capability re-fetch
5656
// Store reference to current voiceConfiguration
5757
const preActivityVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
5858

59-
// Send a regular message (not capabilitiesChanged event)
59+
// Send a regular message (capabilities only update via EventTarget, not activities)
6060
await directLine.emulateIncomingActivity({
6161
type: 'message',
6262
text: 'Hello! This is a regular message.',
@@ -69,13 +69,13 @@
6969
// Get voiceConfiguration after regular activity
7070
const postActivityVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
7171

72-
// Reference should be the same (no re-calculation for regular activities)
72+
// Reference should be the same (activities don't trigger capability re-fetch)
7373
expect(postActivityVoiceConfig).toBe(preActivityVoiceConfig);
7474

75-
// TEST 3: capabilitiesChanged event SHOULD trigger re-calculation
75+
// TEST 3: capabilitieschanged event SHOULD trigger re-fetch
7676
const preChangeVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
7777

78-
// Update capability and emit event
78+
// Update capability and dispatch capabilitieschanged event via EventTarget
7979
directLine.setCapability('getVoiceConfiguration', { voice: 'en-GB', speed: 1.5 }, { emitEvent: true });
8080

8181
// Wait for event to be processed
@@ -92,7 +92,7 @@
9292
// TEST 4: Same value should reuse reference (shallow equality check)
9393
const preNoChangeVoiceConfig = await renderHook(() => useCapabilities(caps => caps.voiceConfiguration));
9494

95-
// Set same value and emit event
95+
// Set same value and dispatch event
9696
directLine.setCapability('getVoiceConfiguration', { voice: 'en-GB', speed: 1.5 }, { emitEvent: true });
9797

9898
// Wait for event to be processed

docs/CAPABILITIES.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,18 @@ if (voiceConfig) {
2929
## How it works
3030

3131
1. **Initial fetch** - When WebChat mounts, it checks if the adapter exposes capability getter functions and retrieves initial values
32-
2. **Event-driven updates** - When the adapter emits a `capabilitiesChanged` event, WebChat re-fetches all capabilities from the adapter
32+
2. **Event-driven updates** - When the adapter dispatches a `capabilitieschanged` event, WebChat re-fetches all capabilities from the adapter
3333
3. **Optimized re-renders** - Only components consuming changed capabilities will re-render
3434

3535
## For adapter implementers
3636

37-
To expose capabilities from your adapter:
37+
To expose capabilities from your adapter, implement event listener methods and provide getter functions.
3838

39-
### 1. Implement getter functions
39+
### 1. Create an EventTarget and implement getter functions
4040

4141
```js
42+
const eventTarget = new EventTarget();
43+
4244
const adapter = {
4345
// ... other adapter methods
4446

@@ -47,21 +49,19 @@ const adapter = {
4749
sampleRate: 16000,
4850
chunkIntervalMs: 100
4951
};
50-
}
52+
},
53+
addEventListener: eventTarget.addEventListener.bind(eventTarget),
54+
removeEventListener: eventTarget.removeEventListener.bind(eventTarget)
5155
};
5256
```
5357

54-
### 2. Emit change events
58+
### 2. Dispatch change events internally
5559

56-
When capability values change, emit a `capabilitiesChanged` event activity:
60+
When capability values change, dispatch a `capabilitieschanged` event using the internal EventTarget:
5761

5862
```js
59-
// When configuration changes, emit the nudge event
60-
adapter.activity$.next({
61-
type: 'event',
62-
name: 'capabilitiesChanged',
63-
from: { id: 'bot', role: 'bot' }
64-
});
63+
// When configuration changes, dispatch the event internally
64+
eventTarget.dispatchEvent(new Event('capabilitieschanged'));
6565
```
6666

6767
WebChat will then call all capability getter functions and update consumers if values changed.

packages/api/src/providers/Capabilities/CapabilitiesComposer.tsx

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import React, { memo, useCallback, useMemo, type ReactNode } from 'react';
2-
import { useReduceMemo } from 'use-reduce-memo';
3-
import type { WebChatActivity } from 'botframework-webchat-core';
4-
import { literal, object, safeParse } from 'valibot';
1+
import React, { memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
52

6-
import useActivities from '../../hooks/useActivities';
73
import useWebChatAPIContext from '../../hooks/internal/useWebChatAPIContext';
84
import CapabilitiesContext from './private/Context';
95
import fetchCapabilitiesFromAdapter from './private/fetchCapabilitiesFromAdapter';
@@ -13,61 +9,39 @@ type Props = Readonly<{ children?: ReactNode | undefined }>;
139

1410
const EMPTY_CAPABILITIES: Capabilities = Object.freeze({});
1511

16-
// Synthetic marker to trigger initial fetch - must be a stable reference
17-
const INIT_MARKER = Object.freeze({ type: 'capabilities:init' as const });
18-
type InitMarker = typeof INIT_MARKER;
19-
type ReducerInput = WebChatActivity | InitMarker;
20-
21-
const CapabilitiesChangedEventSchema = object({
22-
type: literal('event'),
23-
name: literal('capabilitiesChanged')
24-
});
25-
26-
const isInitMarker = (item: ReducerInput): item is InitMarker => item === INIT_MARKER;
27-
28-
const isCapabilitiesChangedEvent = (activity: ReducerInput): boolean =>
29-
safeParse(CapabilitiesChangedEventSchema, activity).success;
30-
3112
/**
32-
* Composer that derives capabilities from the adapter using a pure derivation pattern.
13+
* Composer that provides capabilities from the adapter via EventTarget pattern.
3314
*
3415
* Design principles:
35-
* 1. Initial fetch: Pulls capabilities from adapter on mount via synthetic init marker
36-
* 2. Event-driven updates: Re-fetches only when 'capabilitiesChanged' event is detected
16+
* 1. Initial fetch: Pulls capabilities from adapter on mount
17+
* 2. Event-driven updates: Re-fetches when adapter dispatches 'capabilitieschanged' event
3718
* 3. Stable references: Individual capability objects maintain reference equality if unchanged
3819
* - This ensures consumers using selectors only re-render when their capability changes
3920
*/
4021
const CapabilitiesComposer = memo(({ children }: Props) => {
41-
const [activities] = useActivities();
4222
const { directLine } = useWebChatAPIContext();
4323

44-
const activitiesWithInit = useMemo<readonly ReducerInput[]>(
45-
() => Object.freeze([INIT_MARKER, ...activities]),
46-
[activities]
24+
const getAllCapabilities = useCallback(
25+
() => fetchCapabilitiesFromAdapter(directLine, EMPTY_CAPABILITIES).capabilities,
26+
[directLine]
4727
);
4828

49-
// TODO: [P1] update to use EventTarget than activity$.
50-
const capabilities = useReduceMemo(
51-
activitiesWithInit,
52-
useCallback(
53-
(prevCapabilities: Capabilities, item: ReducerInput): Capabilities => {
54-
const shouldFetch = isInitMarker(item) || isCapabilitiesChangedEvent(item);
29+
const [capabilities, setCapabilities] = useState<Capabilities>(() => getAllCapabilities());
5530

56-
if (!shouldFetch) {
57-
return prevCapabilities;
58-
}
31+
useEffect(() => {
32+
const handleCapabilitiesChange = () => {
33+
setCapabilities(prevCapabilities => {
34+
const { capabilities, hasChanged } = fetchCapabilitiesFromAdapter(directLine, prevCapabilities);
35+
return hasChanged ? capabilities : prevCapabilities;
36+
});
37+
};
5938

60-
const { capabilities: newCapabilities, hasChanged } = fetchCapabilitiesFromAdapter(
61-
directLine,
62-
prevCapabilities
63-
);
39+
if (typeof directLine?.addEventListener === 'function') {
40+
directLine.addEventListener('capabilitieschanged', handleCapabilitiesChange);
6441

65-
return hasChanged ? newCapabilities : prevCapabilities;
66-
},
67-
[directLine]
68-
),
69-
EMPTY_CAPABILITIES
70-
);
42+
return () => directLine.removeEventListener('capabilitieschanged', handleCapabilitiesChange);
43+
}
44+
}, [directLine]);
7145

7246
const contextValue = useMemo(() => Object.freeze({ capabilities }), [capabilities]);
7347

packages/test/page-object/src/globals/testHelpers/createDirectLineEmulator.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,12 @@ export default function createDirectLineEmulator({ autoConnect = true, ponyfill
121121
// Generic capabilities storage
122122
const capabilities = new Map();
123123

124-
// Helper to emit capabilitiesChanged event
124+
// EventTarget for capability change notifications
125+
const eventTarget = new EventTarget();
126+
127+
// Helper to dispatch capabilitieschanged event via EventTarget
125128
const emitCapabilitiesChangedEvent = () => {
126-
activityDeferredObservable.next({
127-
from: { id: 'bot', role: 'bot' },
128-
id: uniqueId(),
129-
name: 'capabilitiesChanged',
130-
timestamp: getTimestamp(),
131-
type: 'event'
132-
});
129+
eventTarget.dispatchEvent(new Event('capabilitieschanged'));
133130
};
134131

135132
const directLine = {
@@ -155,6 +152,8 @@ export default function createDirectLineEmulator({ autoConnect = true, ponyfill
155152
emitCapabilitiesChangedEvent();
156153
}
157154
},
155+
addEventListener: eventTarget.addEventListener.bind(eventTarget),
156+
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
158157
end: () => {
159158
// This is a mock and will no-op on dispatch().
160159
},

0 commit comments

Comments
 (0)