-
Notifications
You must be signed in to change notification settings - Fork 159
chore: unify event emitter #8577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
4e0d57e to
6ccc6d4
Compare
ericpgreen2
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for consolidating the event handling code - having a single pattern across the codebase is a nice improvement.
A few design thoughts:
Composition over inheritance
Extending EventEmitter exposes emit() and clearListeners() as public methods on Conversation, SSEFetchClient, etc. External code can now call conversation.emit("error", "fake") or conversation.clearListeners(), which doesn't seem intentional.
Composition would keep those methods internal:
class Conversation {
private readonly events = new EventEmitter<ConversationEvents>();
public readonly on = this.events.on.bind(this.events);
public readonly once = this.events.once.bind(this.events);
// emit() stays internal via this.events.emit(...)
}Naming
ConversionEvents→ConversationEvents(typo)VoidType = void | Promise<void>- the async return type is misleading sinceemit()doesn't await. Consider removingPromise<void>or renaming to signal fire-and-forget behavior.Argsis generic →EventPayloadorEmitArgswould be clearer
Minor
once()is missing thepublicmodifier (lines 34-41 inevent-emitter.ts)
Aside
Worth knowing about: mitt is a ~200 byte event emitter with good TypeScript support. It lacks once() (we use it once, for eventBus.once("rill-yaml-updated", ...)), but could be worth considering if we ever revisit this. Not suggesting we switch - just noting it exists.
Developed in collaboration with Claude Code
|
Thanks @ericpgreen2. I have addressed your comments. Regarding |
ericpgreen2
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All previous feedback has been addressed - the composition approach looks good.
One observation (non-blocking): the move to events adds subscription management overhead for tightly-coupled consumers. For example, in ChatInput.svelte:
Before:
await currentConversation.sendMessage($additionalContextStore, {
onStreamStart: () => editor.commands.setContent(""),
});After:
let streamStartUnsub: (() => void) | undefined = undefined;
$: {
streamStartUnsub?.();
streamStartUnsub = currentConversation.on("stream-start", () => {
editor.commands.setContent("");
});
}
// + cleanup in onMount returnThe event system shines for external listeners like generate-sample-data.ts that need to observe a conversation they don't own. But for ChatInput, which calls sendMessage directly, the callback was simpler.
These two patterns aren't mutually exclusive - sendMessage could optionally accept callbacks that fire alongside the events:
public async sendMessage(
context: RuntimeServiceCompleteBody,
options?: { onStreamStart?: () => void }
) {
options?.onStreamStart?.(); // Callback for direct callers
this.events.emit("stream-start"); // Event for external listeners
// ...
}This would let ChatInput use the simpler callback pattern while still supporting external listeners via on(). Not a blocker - just something to consider if the subscription boilerplate becomes a pattern elsewhere.
Developed in collaboration with Claude Code
4c79461 to
60803e2
Compare
We have 3 different classes that reimplement event emitter code. We also have the conversation class that uses the callback method for event handling. This PR unifies them by adding an
EventEmitterclass.Used in #8505 to have a separate "listener" to chat conversation messages.
Checklist: