Skip to content

feat(messages): Add typed EventEmitter generics for NDKMessenger and NDKConversation #382

@cardugarte

Description

@cardugarte

Summary

NDKMessenger and NDKConversation both extend EventEmitter from eventemitter3 without type parameters, making all .on(), .off(), and .emit() calls untyped. This means consumers can't get compile-time safety for event names or callback signatures.

Additionally, there's a minor bug in handleIncomingMessage where conversation-created is emitted on every incoming message instead of only on the first message of a new conversation.

Problem

1. Untyped EventEmitter

// Current (src/messenger.ts)
class NDKMessenger extends EventEmitter { ... }

// Consumer gets no type safety:
messenger.on('mesage', (msg) => { ... })  // typo, no error
messenger.on('message', (msg: string) => { ... })  // wrong type, no error

eventemitter3 supports a type map generic that would solve this:

interface NDKMessengerEvents {
  'message': (message: NDKMessage) => void;
  'conversation-created': (conversation: NDKConversation) => void;
  'error': (error: ErrorEvent) => void;
}

class NDKMessenger extends EventEmitter<NDKMessengerEvents> { ... }

Same for NDKConversation:

interface NDKConversationEvents {
  'message': (message: NDKMessage) => void;
  'error': (error: ErrorEvent) => void;
  'state-change': (event: StateChangeEvent) => void;
}

class NDKConversation extends EventEmitter<NDKConversationEvents> { ... }

2. Bug in conversation-created emission

In src/messenger.ts, handleIncomingMessage:

// Current (line ~840)
if (conversation.getMessages.length === 1) {
  this.emit("conversation-created", conversation);
}

conversation.getMessages.length reads the function arity (number of parameters of getMessages, which is always 1 because of the limit? param), not the message count. This means conversation-created fires on every incoming message.

Suggested fix:

const messages = await conversation.getMessages();
if (messages.length === 1) {
  this.emit("conversation-created", conversation);
}

Workaround

We're currently using module augmentation in our project to add type safety locally:

// messenger-events.d.ts
declare module '@nostr-dev-kit/messages' {
  interface NDKMessengerEvents { ... }
  interface NDKConversationEvents { ... }
  // augment classes to use typed generics
}

Offer

If you agree with these changes, I'd be happy to submit a PR addressing both issues. The scope would be:

  1. Add event type interfaces (NDKMessengerEvents, NDKConversationEvents)
  2. Apply them as generics to EventEmitter<T>
  3. Fix the conversation-created emission logic
  4. Export the event interfaces for consumers

Let me know!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions