Skip to content

Migrate core event system from EventEmitter3 to Eventa#567

Open
luoling8192 wants to merge 4 commits intomainfrom
claude/refactor-event-system-Nv6Uc
Open

Migrate core event system from EventEmitter3 to Eventa#567
luoling8192 wants to merge 4 commits intomainfrom
claude/refactor-event-system-Nv6Uc

Conversation

@luoling8192
Copy link
Copy Markdown
Collaborator

@luoling8192 luoling8192 commented Mar 2, 2026

Summary

This PR migrates the core event system from EventEmitter3 to @moeru/eventa, a type-safe event framework. The migration introduces a cleaner separation between fire-and-forget events and RPC-style invoke handlers, with centralized event definitions and improved type safety across the codebase.

Key Changes

  • Event System Overhaul: Replaced EventEmitter3 with @moeru/eventa's EventContext and defineEventa/defineInvokeEventa APIs

    • Removed CoreEventType enum and replaced with individual typed event definitions
    • Created modular event files (auth.ts, message.ts, storage.ts, dialog.ts, etc.) in packages/core/src/events/
    • Added dispatch.ts with centralized event mapping for wire protocol bridging
  • Event Handler Refactoring: Updated all event handlers to use new APIs

    • Converted ctx.emitter.on() to onEvent() helper or defineInvokeHandler() for RPC handlers
    • Changed error handling from ctx.withError() + return to throwing errors in invoke handlers
    • Updated response patterns: invoke handlers now return results instead of emitting separate events
  • RPC vs Fire-and-Forget Separation: Clearly distinguished between two event patterns

    • RPC invokes (e.g., storageFetchMessagesInvoke) use defineInvokeHandler() and return responses
    • Fire-and-forget events (e.g., messageFetchEvent) use onEvent() and emit without waiting
    • Added rpcEvents and fireAndForgetEvents maps for wire protocol dispatch
  • Server-Side Event Dispatch: Created apps/server/src/event-dispatch.ts to bridge JSON-over-WebSocket to Eventa

    • Handles both RPC invokes (awaits response, sends back to peer) and fire-and-forget events
    • Replaces previous registerCoreEventListeners() pattern with dispatchClientEvent()
  • Client Adapter Updates: Updated packages/client/src/adapters/core-bridge.ts to use new event maps

    • Simplified event registration using fireAndForgetEvents, notificationEvents, and rpcEvents
    • Removed type-based event filtering in favor of string-based event name checks
  • Utility Helpers: Added onEvent() and onceEvent() wrappers in packages/core/src/utils/promise.ts

    • Unwraps Eventa's optional body payload for cleaner handler signatures
    • Replaces previous waitForEvent() implementation
  • Test Updates: Updated test files to use new event APIs

    • Replaced CoreEventType references with imported event objects
    • Updated mock emitter creation to use createContext() from Eventa
  • Removed: Deleted packages/core/src/utils/memory-leak-detector.ts (no longer needed with Eventa)

Notable Implementation Details

  • Event definitions are now co-located with their types in packages/core/src/events/
  • The dispatch.ts file serves as a single source of truth for wire protocol event mapping
  • Error handling in RPC handlers now uses exceptions instead of side-effect emissions
  • The migration maintains backward compatibility at the wire protocol level through the dispatch maps
  • All event handlers are now type-safe with proper TypeScript inference of payload types

https://claude.ai/code/session_01JgqUCyTk9N37FSGY6nc97W


Note

High Risk
High risk because it rewires the core cross-cutting event bus (auth, storage, sync, takeout, message processing) and changes server/client WebSocket dispatch semantics, including new RPC-style invoke/return flows and error propagation via thrown exceptions.

Overview
Migrates the core event system from EventEmitter3 to @moeru/eventa, introducing a typed CoreContext.ctx and modular event definitions under packages/core/src/events/* (plus shared fireAndForgetEvents/rpcEvents/notificationEvents maps).

Refactors core handlers and services to use onEvent/onceEvent and defineInvokeHandler, shifting several flows from “emit response events” to RPC-style invoke handlers that return results (notably storage and message summary/unread paths), and updates error handling in invokes to throw for unauthorized/failed operations.

Updates the server WebSocket bridge to dispatch incoming messages via a new event-dispatch.ts (RPC invokes awaited and replied to the calling peer; fire-and-forget emitted), and registers all core notification broadcasts upfront per account (deprecating the runtime server:event:register listener-registration behavior). Bot and client adapters are updated accordingly, tests are rewritten for Eventa contexts, @moeru/eventa is added across packages, and the old memory leak detector is removed.

Written by Cursor Bugbot for commit d2a5cb5. This will update automatically on new commits. Configure here.

claude added 2 commits March 2, 2026 09:24
Replace eventemitter3-based event bus with @moeru/eventa for type-safe
events and RPC patterns in the core package.

Phase 1: Define all Eventa events across 14 domain files in events/
Phase 2: Replace CoreContext.emitter with EventContext from Eventa
Phase 3: Migrate all event handlers, services, and tests

- Convert 13 implicit request-response pairs to defineInvokeEventa RPC
- Convert ~40 fire-and-forget events to defineEventa
- Update all 10 handler files, 6 service files, and 7 test files
- Install @moeru/eventa in server and client packages

Old CoreEventType enum and types/events.ts retained for server/client
packages (to be removed in later phases).

https://claude.ai/code/session_01JgqUCyTk9N37FSGY6nc97W
Migrate server WebSocket bridge, bot package, and client core-bridge to
use @moeru/eventa event system, completing the Phase 4 refactor:

- Create shared event dispatch maps (fireAndForgetEvents, rpcEvents,
  notificationEvents) in packages/core/src/events/dispatch.ts
- Refactor server app.ts and event-dispatch.ts to use shared maps
  for dispatching WS messages to Eventa events
- Refactor server account.ts with notification event broadcasting
  via typed Eventa event objects
- Migrate bot bridge.ts, export.ts, and summary.ts to Eventa events
  (summary.ts uses defineInvoke for RPC)
- Fix client core-bridge.ts to use Eventa context (ctx.ctx) instead
  of removed emitter property
- Add onEvent/onceEvent/waitForEvent helpers in utils/promise.ts for
  safe body unwrapping (Eventa wraps payload in { body?: P })
- Fix CoreContext.ctx type to EventContext<any, any> for
  InvocableEventContext compatibility
- Fix void event emits to pass explicit undefined
- Remove unused memory-leak-detector.ts
- Simplify server events.ts types with Parameters<> pattern
- All event handler files updated to use onEvent helper
- All tests passing, typecheck clean across all packages

https://claude.ai/code/session_01JgqUCyTk9N37FSGY6nc97W
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the summary. You can try again by commenting /gemini summary.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4386ea346

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 130 to 134
function cleanup() {
logger.debug('Cleaning up CoreContext')

// Clean up memory leak detector first
cleanupMemoryLeakDetector()

// Remove all event listeners
emitter.removeAllListeners()

// Clear event sets
toCoreEvents.clear()
fromCoreEvents.clear()
useLogger('core:context').debug('Cleaning up CoreContext')

// Clear client reference
// @ts-expect-error - Allow setting to undefined for cleanup
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Detach Eventa listeners during CoreContext cleanup

destroyCoreInstance() still treats ctx.cleanup() as the final teardown path, but cleanup() now only clears local references and never unregisters Eventa subscriptions. Because many handlers run async fire-and-forget (for example logout/takeout flows), in-flight work can still emit into this context after destruction and trigger stale handlers/broadcast closures, causing post-logout side effects and retained account-scoped references that the previous removeAllListeners() cleanup prevented.

Useful? React with 👍 / 👎.

Comment on lines +90 to +92
{ event: messageDataEvent, wireName: 'message:data' },
{ event: messageFetchProgressEvent, wireName: 'message:fetch:progress' },
{ event: dialogAvatarDataEvent, wireName: 'dialog:avatar:data' },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Forward message:processed in server notification bridge

The new server-side static notificationEvents list omits message:processed, even though this commit still defines it as a core notification in the shared dispatch map. In WebSocket/server mode, clients that register for message:processed will no longer receive batch completion events, creating a behavior regression and inconsistency versus core-bridge mode where this notification is still wired.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 3 potential issues.

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


logger.withFields({ accountId: dbAccount.id }).verbose('Set current account ID')

ctx.emitter.emit(CoreEventType.AccountReady, { accountId: dbAccount.id })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bootstrap fetchDialogs call no longer persists dialogs to database

High Severity

The refactored fetchDialogs now only returns { dialogs, accountId } instead of emitting StorageRecordDialogs as a side effect. The bootstrap call in afterConnectedEventHandler discards this return value, so dialogs fetched during initial account connection are never persisted to the database. The RPC handler in registerDialogEventHandlers does emit storageRecordDialogsEvent, but that handler isn't used for the bootstrap path — and it's registered after the bootstrap call completes.

Additional Locations (1)

Fix in Cursor Fix in Web

ctx.emitter.on(CoreEventType.TakeoutStatsFetch, async ({ chatId }) => {
await takeoutService.fetchChatSyncStats(chatId)
defineInvokeHandler(ctx.ctx, takeoutStatsFetchInvoke, async ({ chatId }) => {
return await takeoutService.fetchChatSyncStats(chatId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RPC handler returns undefined on caught service error

Medium Severity

fetchChatSyncStats catches errors internally and returns undefined from the catch block. The new defineInvokeHandler wrapping this call will send undefined as the RPC response to the client. Previously, errors were silently swallowed with no event emitted. Now the client receives undefined instead of a ChatSyncStats object, likely causing destructuring errors on the receiving end. The catch block needs to either rethrow or return a default value.

Additional Locations (1)

Fix in Cursor Fix in Web

currentAccountId = undefined

logger.debug('CoreContext cleaned up')
useLogger('core:context').debug('CoreContext cleaned up')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cleanup no longer removes event listeners causing memory leak

High Severity

The cleanup() function previously called emitter.removeAllListeners() to tear down all event subscriptions when an account logs out. That line was removed during the migration, and no Eventa equivalent was added. All onEvent, ctx.ctx.on, and defineInvokeHandler registrations remain active on the orphaned Eventa context after destroyCoreInstance runs, leaking memory. The destroyCoreInstance docstring even states it "Removes ALL event listeners from emitter" and "Disposes Eventa context listeners" but the implementation no longer does either.

Additional Locations (1)

Fix in Cursor Fix in Web

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.

2 participants