event-bus
This feature lets one remote tell another that something happened without either importing the other. They publish and listen to named events, and a shared contracts package holds the payload types. A wrong name or payload fails your typecheck.
What you get
A typed publish and subscribe bus, a packages/contracts workspace package where event types live, and a sample event so you can see it working. Remotes never import each other (decision 0009), so the contracts package is the one thing they share. It is off by default.
Add it
What changed:
Every app gets the event bus package and your contracts package as dependencies. A unit test sits next to each events.ts if you have the testing feature.
Adding it to an existing project works the same way. A remote you add later gets its own events.ts and the dependency.
Use it
Define an event
Events are declared as types in the contracts package. This is the generated file:
Add a line for yours:
The file holds types only and imports nothing, so reading it costs a remote nothing at run time.
Publish from one remote, listen in another
Outside React, use subscribe(name, handler), which returns the function that stops listening. A wrong name, a wrong payload or a missing payload fails pnpm typecheck.
Give late subscribers the latest value
Most events are things that happened, and a remote that mounts late missed them. Some are state instead, like the signed-in user. Mark those, and each new subscriber gets the last payload immediately. The generated shell file already does this for the sample event:
Do it once, in the shell. Under React StrictMode a replayed event reaches a handler once per mount, so keep that handler safe to run twice. Live events are delivered once.
Check payloads at run time
TypeScript checks the code you wrote, but it cannot check a payload from a remote built last week. Give the event a validate function. Nothing validates in the core bundle unless you do:
fromStandardSchema accepts any Standard Schema (Zod 3.24+, Valibot 1+, ArkType 2+). A rejected payload reaches nobody, publish returns false, and the console names the event and the schema's message, never the payload. The schema must validate synchronously.
Configure it
feature.json has no prompts. Behavior is set per event with configureEvent(name, options):
A subscriber can opt out of a replayed payload with subscribe(name, handler, { replay: false }). Event names look like <domain>.<event>.v<major>, for example cart.item-added.v1. A name that does not match warns in development.
How it works
Every app merges the contracts' Events into the bus's map with one declare module block. That makes publishing and subscribing typed end to end.
The shell's events.ts imports the bus, which makes the shell the one that provides the shared copy. The bus and its React entry are shared singletons, so every remote uses the shell's copy. The registry of subscribers also lives on globalThis, so a second copy of the same version still reaches the same subscribers. A copy that finds a registry from an incompatible version keeps its own and exchanges events with other copies through window events.
A handler that throws is logged and does not stop the other handlers.
Gotchas
Change an event's payload in a way old listeners cannot read by adding a new version, such as cart.item-added.v2. Publish both until every listener has moved, then delete the old one. Editing v1 in place breaks remotes that were built before the change.
-
Two copies of the bus. In development you get a warning that a remote is bundling its own copy. Events still arrive, but share the package as a singleton and make sure the shell imports it.
-
Events in
mfeflow.app.json. The two lists inmfeflow.app.jsontake the same versioned names, checked by the same pattern. The@majorform is not accepted. -
Replay keeps only the last payload. Do not turn it on for events like
cart.item-added.v1, where every occurrence matters.
Reference
- mfeflow.app.json, where a remote lists what it publishes and consumes
- Ownership
- Shared singletons
@mfeflow/event-busREADME