Skip to content

Conversation

@AdityaHegde
Copy link
Collaborator

@AdityaHegde AdityaHegde commented Dec 30, 2025

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 EventEmitter class.

Used in #8505 to have a separate "listener" to chat conversation messages.

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

@AdityaHegde AdityaHegde force-pushed the chore/unify-event-emitter branch from 4e0d57e to 6ccc6d4 Compare December 30, 2025 05:46
Copy link
Contributor

@ericpgreen2 ericpgreen2 left a 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

  • ConversionEventsConversationEvents (typo)
  • VoidType = void | Promise<void> - the async return type is misleading since emit() doesn't await. Consider removing Promise<void> or renaming to signal fire-and-forget behavior.
  • Args is generic → EventPayload or EmitArgs would be clearer

Minor

  • once() is missing the public modifier (lines 34-41 in event-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

@AdityaHegde
Copy link
Collaborator Author

Thanks @ericpgreen2. I have addressed your comments.

Regarding mitt, I like the current API where on returns unsub method, fits svelte better. mitt has the off instead that needs a bit more code. They also seem to not want to support once developit/mitt#136, we would need to implement ourselves.

Copy link
Contributor

@ericpgreen2 ericpgreen2 left a 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 return

The 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

@AdityaHegde AdityaHegde force-pushed the chore/unify-event-emitter branch from 4c79461 to 60803e2 Compare January 7, 2026 05:48
@AdityaHegde AdityaHegde merged commit 886a325 into main Jan 7, 2026
13 of 14 checks passed
@AdityaHegde AdityaHegde deleted the chore/unify-event-emitter branch January 7, 2026 08:03
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.

3 participants