refactor!: migrate event bus from mitt to eventemitter3 (EMB-84)#740
Merged
Conversation
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.
E2E Examples — failuresThe following example(s) failed:
See the workflow run for Playwright reports and logs. |
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
approved these changes
May 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which Linear task is linked to this PR?
EMB-84 — Migrate from mitt to eventemitter3 in the widget
Why was it implemented this way?
mittandeventemitter3are both viable; the migration pickseventemitter3for 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
eventemitter3directly — no compatibility wrapper. The exportedEmitter<...>type is replaced withEventEmitter<...>(re-exported asWidgetEventEmitter/WalletManagementEventEmitterfor cleaner integrator imports).WidgetEvents/WalletManagementEventsfrom{ key: Payload }to eventemitter3's function-listener form{ key: (data: Payload) => void }.on('*', ...)) inWidgetEventsBridgewith per-event registration driven byObject.values(WidgetEvent). ThecontactSupportnoop-handler dance is no longer needed.Exact<>invariant in bothevents.tsfiles that fails the build if the*Eventenum andkeyof *Eventsever drift apart. This caught a real pre-existing bug:WidgetEvents.walletConnectedwas declared but never emitted onwidgetEvents— dead entry removed.widgetEvents.all.has(...)→widgetEvents.listenerCount(...) === 0inuseContactSupport.emitter.all.clear()→emitter.removeAllListeners()in the privy / privy-ethers examples.mittdeclaration fromdynamic,connectkit, andreown(they never imported it).Test coverage
packages/widget/src/hooks/useWidgetEvents.test.ts) emits everyWidgetEventvalue throughwidgetEventsand asserts the listener fires; also verifieslistenerCountsemantics aroundon/off.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
WildcardEventEmitterwrapper subclassing eventemitter3 to preserve theon('*', ...)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 viaObject.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) toEventEmitter<WidgetEvents>(eventemitter3). The event-map types are now function-listener-shaped. Integrators referencing these directly should switch to the re-exportedWidgetEventEmitter/WalletManagementEventEmitteraliases.Checklist before requesting a review