Skip to content

refactor!: migrate event bus from mitt to eventemitter3 (EMB-84)#740

Merged
chybisov merged 5 commits into
mainfrom
chore/migrate-mitt-to-eventemitter3
May 26, 2026
Merged

refactor!: migrate event bus from mitt to eventemitter3 (EMB-84)#740
chybisov merged 5 commits into
mainfrom
chore/migrate-mitt-to-eventemitter3

Conversation

@chybisov
Copy link
Copy Markdown
Member

Which Linear task is linked to this PR?

EMB-84 — Migrate from mitt to eventemitter3 in the widget

Why was it implemented this way?

mitt and eventemitter3 are both viable; the migration picks eventemitter3 for ecosystem alignment (it's the de facto choice in viem / wagmi / socket.io and is already pulled in transitively) and to drop mitt's NodeNext default-export TypeScript workaround. The trade-offs are documented in EMB-84 — net cost is +~928 B gzip and a public API breaking change.

Approach

  • Use eventemitter3 directly — no compatibility wrapper. The exported Emitter<...> type is replaced with EventEmitter<...> (re-exported as WidgetEventEmitter / WalletManagementEventEmitter for cleaner integrator imports).
  • Reshape WidgetEvents / WalletManagementEvents from { key: Payload } to eventemitter3's function-listener form { key: (data: Payload) => void }.
  • Replace mitt's wildcard listener (on('*', ...)) in WidgetEventsBridge with per-event registration driven by Object.values(WidgetEvent). The contactSupport noop-handler dance is no longer needed.
  • Add a TypeScript Exact<> invariant in both events.ts files that fails the build if the *Event enum and keyof *Events ever drift apart. This caught a real pre-existing bug: WidgetEvents.walletConnected was declared but never emitted on widgetEvents — dead entry removed.
  • widgetEvents.all.has(...)widgetEvents.listenerCount(...) === 0 in useContactSupport.
  • emitter.all.clear()emitter.removeAllListeners() in the privy / privy-ethers examples.
  • Drop the unused mitt declaration from dynamic, connectkit, and reown (they never imported it).

Test coverage

  • New vitest suite (packages/widget/src/hooks/useWidgetEvents.test.ts) emits every WidgetEvent value through widgetEvents and asserts the listener fires; also verifies listenerCount semantics around on / off.
  • The Exact<> invariant is the primary protection against forgetting to wire a new event in the bridge — adding to either the enum or the event map without the other fails the build.

Approach considered but rejected

A thin WildcardEventEmitter wrapper subclassing eventemitter3 to preserve the on('*', ...) call site. Smaller diff, but kept the same code shape mitt forced and didn't capture the upside of eventemitter3's idioms. The bridge rewrite via Object.values(WidgetEvent) is the more idiomatic move.

Visual showcase (Screenshots or Videos)

Verified locally against pnpm dev (playground at :3000) — events fire correctly through the new emitter, dev-controls toggles work, no console errors mentioning the migration-relevant terms (mitt, Emitter, .all, listenerCount, eventemitter3, "is not a function").

BREAKING CHANGE

The exported emitter type changes from Emitter<WidgetEvents> (mitt) to EventEmitter<WidgetEvents> (eventemitter3). The event-map types are now function-listener-shaped. Integrators referencing these directly should switch to the re-exported WidgetEventEmitter / WalletManagementEventEmitter aliases.

Checklist before requesting a review

  • I have performed a self-review and testing of my code.
  • This pull request is focused and addresses a single problem.
  • If this PR modifies the Widget API or adds new features that require documentation, I have updated the documentation in the public-docs repository.

Replaces mitt with eventemitter3 across @lifi/widget,
@lifi/wallet-management and the privy/privy-ethers examples; drops
the unused mitt declaration from dynamic/connectkit/reown.

Internal changes:
- `widgetEvents` and the wallet-management emitter are now
  `EventEmitter` instances from eventemitter3; the mitt NodeNext
  default-export cast workaround is gone.
- `WidgetEvents` / `WalletManagementEvents` are reshaped from
  `{ key: Payload }` to the function-listener shape eventemitter3
  expects: `{ key: (data: Payload) => void }`.
- New `Exact<>` compile-time invariant in both `events.ts` files
  enforces that the `*Event` enum values and `keyof *Events` stay in
  lockstep. The check immediately surfaced a pre-existing drift —
  `WidgetEvents.walletConnected` was declared but never emitted on
  `widgetEvents` (the real event lives on `walletMgmtEvents`); the
  dead entry has been removed.
- `WidgetEventsBridge` no longer uses `on('*', ...)`. It iterates
  `Object.values(WidgetEvent)` to register one handler per event,
  so adding a new event to the enum automatically forwards it
  through the iframe bridge. The `contactSupport` noop-handler
  dance is removed; real per-event listeners satisfy
  `useContactSupport`'s `listenerCount(...) === 0` gate.
- `useContactSupport` switches from `widgetEvents.all.has(...)` to
  `widgetEvents.listenerCount(...) === 0`.
- Example providers replace `mitt.all.clear()` with
  `emitter.removeAllListeners()`.
- New vitest suite covers emit/on/off/listenerCount for every value
  in `WidgetEvent`.

BREAKING CHANGE: The exported `widgetEvents` / `useWidgetEvents`
emitter is now `EventEmitter<WidgetEvents>` from eventemitter3
rather than `Emitter<WidgetEvents>` from mitt. `WidgetEvents` /
`WalletManagementEvents` are now function-listener-shaped, and the
unused `WidgetEvents.walletConnected` entry has been removed.
Integrators referencing these types should switch to the
re-exported `WidgetEventEmitter` / `WalletManagementEventEmitter`
aliases.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 25, 2026

E2E Examples — failures

The following example(s) failed:

  • nft-checkout
  • vite-iframe-wagmi

See the workflow run for Playwright reports and logs.

chybisov added 4 commits May 25, 2026 13:59
The mitt-era bridge used `widgetEvents.on('*', ...)` plus a per-event
noop-handler dance to keep `useContactSupport`'s `all.has(...)` gate
honest: wildcard listeners register under `*` in mitt, not under
specific event keys, so the gate would (correctly) say "nobody is
listening" unless the host had subscribed to `contactSupport`
specifically. The dance ensured the gate matched the host's intent.

After the mitt → eventemitter3 swap, the bridge blanket-attached one
handler per `WidgetEvent` value whenever the host subscribed to any
event. That made `widgetEvents.listenerCount('contactSupport') > 0`
unconditionally, so `useContactSupport` always emitted instead of
falling back to opening `help.li.fi` — a regression for hosts that
never subscribed to `contactSupport`.

Fix: attach widget-side listeners only for events the host has
actually subscribed to. The bridge now mirrors `bridge.getSubscribedEvents()`
via per-event `attachEvent` / `detachEvent`, driven by
`onWidgetEventSubscriptionChange`. `listenerCount(name)` now reflects
real integrator intent, so the gate works correctly without any
special-case for `contactSupport` and the design generalizes to any
future "is-anyone-listening?" event.
Symmetric to the widget side: build a `walletEventNames` Set from
`Object.values(WalletManagementEvent)` and dispatch via a handler
Map. Drops the four hardcoded `'walletConnected'` / `'walletDisconnected'`
branches. The `Exact<>` invariant in wallet-management's events.ts
keeps the Set in lockstep with `keyof WalletManagementEvents`, so new
wallet events auto-flow through the bridge.
- packages/widget/package.json: move eventemitter3 above i18next so
  the dependencies block is alphabetical (Biome doesn't sort npm
  deps, hence the slip).
- packages/widget/src/types/events.ts: drop the unused
  `WalletConnected` type. It was only referenced by the dead
  `WidgetEvents.walletConnected` entry that the `Exact<>` invariant
  flagged in the main commit. Wallet-connection payloads now flow
  exclusively through `@lifi/wallet-management`'s `WalletConnected`,
  which has the correct (stricter) shape. Also drops the now-unused
  `ChainType` import.
- packages/wallet-management/src/index.ts: switch the events hook
  from `export *` to a named export
  (`useWalletManagementEvents`, `widgetEvents`, type
  `WalletManagementEventEmitter`), mirroring the widget package's
  pattern in `packages/widget/src/index.ts`.

BREAKING CHANGE: `WalletConnected` is no longer exported from
`@lifi/widget`. Integrators should import `WalletConnected` from
`@lifi/wallet-management` (different, stricter shape) — or use
`WidgetLightWalletConnected` from `@lifi/widget-light` for the
iframe protocol.
…tEvents

The emitter exported from @lifi/wallet-management was historically
named `widgetEvents` — same identifier as @lifi/widget's emitter,
which forced consumers to import-alias it (e.g.
`widgetEvents as walletMgmtEvents` in the widget-embedded bridge).
Rename to `walletManagementEvents` so the symbol is self-explanatory
and consumers no longer need an alias.

- `packages/wallet-management/src/hooks/useWalletManagementEvents.ts`:
  rename `widgetEvents` → `walletManagementEvents`
- `packages/wallet-management/src/index.ts`: update named re-export
- `packages/widget-embedded/src/providers/WidgetEventsBridge.tsx`:
  drop the import alias; rename local references

BREAKING CHANGE: `widgetEvents` is no longer exported from
@lifi/wallet-management; use `walletManagementEvents` instead.
The widget package's `widgetEvents` export is unchanged.
@effie-ms effie-ms self-requested a review May 26, 2026 15:06
@chybisov chybisov merged commit 4b867dd into main May 26, 2026
19 of 21 checks passed
@chybisov chybisov deleted the chore/migrate-mitt-to-eventemitter3 branch May 26, 2026 15:15
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