Skip to content

Refactor React sub-providers to use conversation.on() subscriptions#749

Draft
kraenhansen wants to merge 7 commits into
kh/event-emitterfrom
kh/event-emitter-react
Draft

Refactor React sub-providers to use conversation.on() subscriptions#749
kraenhansen wants to merge 7 commits into
kh/event-emitterfrom
kh/event-emitter-react

Conversation

@kraenhansen

@kraenhansen kraenhansen commented May 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Sub-providers (ConversationStatus, ConversationMode, ConversationFeedback, ConversationInput) now subscribe to events via conversation.on() instead of composed callback objects
  • ConversationStatusProvider derives status from isStarting/startupError context values instead of onStatusChange callbacks
  • useConversation hook composes callbacks via mergeOptions instead of useRegisterCallbacks
  • Deletes ListenerMap, ListenerSet and their tests (net -419 lines)
  • Adds shared test-utils.ts with mock conversation that supports on()/__emit()

Test plan

  • All 111 React package tests pass
  • Build succeeds (pnpm turbo build)
  • Type-check passes (tsc --noEmit)
  • Lint passes (ESLint + Prettier)
  • Dependent packages (react-native, convai-widget-core) build successfully
  • Manual: Run examples/agent-testbench, verify status/mode/mute/feedback/messages/disconnect

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk because it changes the conversation callback delivery mechanism to a shared event-emitter model and refactors React provider lifecycle/state wiring, which could affect listener ordering and session cleanup behavior.

Overview
Adds a typed event-emitter API (conversation.on/off) to @elevenlabs/client conversations and routes all existing constructor callbacks through this emitter, allowing multiple listeners per event with unsubscribe support.

Introduces AsyncAPI-based codegen (generate-events) to generate wire-event maps/dispatch helpers and moves previously hand-maintained shared conversation types (Role, Mode, Status, Callbacks, etc.) out of @elevenlabs/types into @elevenlabs/client.

Refactors @elevenlabs/react sub-providers to subscribe via conversation.on() (and derives status from new isStarting/startupError context state), removes the internal ListenerMap/ListenerSet callback-composition utilities, and updates tests to use a shared mock conversation with __emit.

Reviewed by Cursor Bugbot for commit 39282de. Bugbot is set up for automated code reviews on this repo. Configure here.

kraenhansen and others added 7 commits May 7, 2026 13:21
Move AsyncAPI schemas from packages/types/schemas/ to schemas/ at
repo root, with symlinks from both packages. Move manually maintained
types (Role, Mode, Status, Callbacks, etc.) from @elevenlabs/types to
@elevenlabs/client, making the types package generated-code-only.

This is preparation for event codegen in the client package, which
needs direct access to the schemas.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Records the removal of manual types from @elevenlabs/types as a minor
bump (pre-v1). Consumers should import these types from
@elevenlabs/client instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `scripts/generate-events.ts` to the client package that reads
`agent.asyncapi.yaml` and generates:
- `GeneratedEventMap` with typed event callbacks for each wire event
- `GeneratedWireType` union of all generated wire type strings
- `WIRE_TYPE_TO_EVENT_NAME` / `WIRE_TYPE_TO_PAYLOAD_FIELD` lookup tables
- `dispatchGeneratedEvent()` helper for runtime dispatch

Internally handled events (audio, agent_response, user_transcript,
client_tool_call, ping, error, internal_tentative_agent_response) are
excluded — they need custom dispatch logic in BaseConversation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Callbacks and CALLBACK_KEYS from types.ts to a new events.ts module
that combines generated events with hand-written internal and extra events
into a unified ConversationEventMap. Add CALLBACK_KEY_TO_EVENT_NAME mapping
for wiring callbacks to event names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace direct this.options.onXxx() callback invocations with
this.emit() calls through a typed event emitter. Constructor-provided
callbacks are automatically registered as initial listeners.

The emitter supports multiple listeners per event, with on() returning
an unsubscribe function for ergonomic cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace this.options.onAudio/onAudioAlignment/onError in
  VoiceConversation with this.emit() calls
- Remove no-op callback defaults from getFullOptions (the emitter
  handles the no-listeners case)
- Keep pre-instance callbacks (onStatusChange, onConnect) as direct
  calls since errors during startSession must propagate to the caller

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ions

Replace ListenerMap/ListenerSet callback composition with direct event
subscriptions on the conversation instance. Sub-providers now subscribe
via conversation.on() and derive status from isStarting/startupError
context values. Hook-level callbacks compose via mergeOptions instead
of the removed registerCallbacks mechanism.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kraenhansen
kraenhansen marked this pull request as draft May 7, 2026 14:22

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 39282de. Configure here.

return conversation.on("error", msg => {
setErrorMessage(msg);
});
}, [conversation]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status stuck in "error" after runtime error then disconnect

High Severity

When conversation becomes null (disconnect), the error-subscription effect early-returns without clearing errorMessage. Since the status derivation checks errorMessage !== undefined before checking conversation, the status stays "error" instead of transitioning to "disconnected". All other sub-providers (ConversationModeProvider, ConversationFeedbackProvider, ConversationInputProvider) correctly reset their state when conversation is null, but ConversationStatusProvider does not.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39282de. Configure here.

} else {
emit(eventName);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated dispatchGeneratedEvent function is never imported or used

Low Severity

dispatchGeneratedEvent is exported from generated/dispatch.ts but is never imported anywhere in the codebase. The onMessageHandler in BaseConversation still uses explicit switch/case statements for all event types. This is dead generated code that adds maintenance burden without being used.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39282de. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant