Mediator Pattern
Define a central coordinator that handles communication between objects so they don't reference each other directly.
The Problemβ
You're building a flight dashboard for air traffic control. Three panels: flight list, weather, and runway status. When the weather changes to "storm", some flights should be delayed and the runway should switch to reduced capacity. When you click a flight, the weather panel should zoom to its destination and the runway panel should show the gate.
The naive approach:
function updateWeather(condition: string) {
weatherPanel.display(condition);
flightList.delayFlightsFor(condition); // knows about flightList
runwayStatus.adjustCapacity(condition); // knows about runwayStatus
}
function selectFlight(flightId: string) {
flightList.highlight(flightId);
weatherPanel.zoomTo(flight.destination); // knows about weatherPanel
runwayStatus.showGate(flight.gate); // knows about runwayStatus
}
Every panel knows about every other panel. Adding a fourth panel (passenger info, fuel status) means modifying all existing panels.
The Solutionβ
Panels only know the mediator. They emit events, the mediator routes them:
import { createMediator } from "@pithos/core/eidos/mediator/mediator";
type DashboardEvents = {
weatherChanged: { condition: string; severity: number };
flightSelected: { flightId: string; destination: string; gate: string };
runwayUpdated: { capacity: "full" | "reduced" | "closed" };
};
const dashboard = createMediator<DashboardEvents>();
// Weather panel reacts to flight selection
dashboard.on("flightSelected", ({ destination }) => {
weatherPanel.zoomTo(destination);
});
// Flight list reacts to weather changes
dashboard.on("weatherChanged", ({ condition, severity }) => {
if (severity > 7) flightList.delayAll();
dashboard.emit("runwayUpdated", { capacity: "reduced" });
});
// Runway reacts to weather
dashboard.on("runwayUpdated", ({ capacity }) => {
runwayStatus.setCapacity(capacity);
});
// Panels emit events without knowing who listens
weatherPanel.onChange = (condition, severity) =>
dashboard.emit("weatherChanged", { condition, severity });
Panels are decoupled. Add a fuel panel without touching weather, flights, or runway.
Live Demoβ
DGAC flight dashboard where weather, flights, and runway panels communicate exclusively through a mediator, with every event visible in a live log.
- dashboard.ts
- Usage
/**
* DGAC Flight Dashboard: Mediator demo.
*
* Three panels (flights, weather, runway) communicate exclusively
* through a typed mediator. No panel knows about the others.
*/
import { createMediator } from "@pithos/core/eidos/mediator/mediator";
import { INITIAL_FLIGHTS, WEATHER_SEVERITY, CAPACITY_FROM_SEVERITY, applyWeatherToFlights } from "./data";
import type { WeatherCondition, RunwayCapacity, DashboardEvents, DashboardState } from "./types";
// Re-export for consumers
export type { WeatherCondition, RunwayCapacity, Flight, LogEntry, DashboardState } from "./types";
export { WEATHER_OPTIONS } from "./types";
export function createDashboard() {
const mediator = createMediator<DashboardEvents>();
let flights = INITIAL_FLIGHTS.map((f) => ({ ...f }));
let weather: WeatherCondition = "clear";
let severity = 0;
let runway: RunwayCapacity = "full";
let selectedFlightId: string | null = null;
let logId = 0;
const logs: { id: number; timestamp: string; event: string; detail: string }[] = [];
let snapshot = buildSnapshot();
const listeners = new Set<() => void>();
function now() {
return new Date().toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
function addLog(event: string, detail: string) {
logs.push({ id: ++logId, timestamp: now(), event, detail });
if (logs.length > 50) logs.shift();
}
function buildSnapshot(): DashboardState {
return {
flights: flights.map((f) => ({ ...f })),
weather,
severity,
runway,
selectedFlightId,
logs: [...logs],
};
}
function commit() {
snapshot = buildSnapshot();
for (const fn of listeners) fn();
}
// ββ Wire up mediator handlers ββββββββββββββββββββββββββββββββββ
mediator.on("weatherChanged", ({ condition, severity: sev }) => {
weather = condition;
severity = sev;
const changes = applyWeatherToFlights(flights, condition);
for (const c of changes) addLog("flightStatusChanged", c);
const cap = CAPACITY_FROM_SEVERITY(sev);
mediator.emit("runwayUpdated", { capacity: cap });
addLog("weatherChanged", `${condition} (severity ${sev})`);
});
mediator.on("runwayUpdated", ({ capacity }) => {
runway = capacity;
addLog("runwayUpdated", `capacity β ${capacity}`);
});
mediator.on("flightSelected", ({ flightId, destination, gate }) => {
selectedFlightId = flightId;
addLog("flightSelected", `${flightId} β ${destination} (gate ${gate})`);
});
mediator.on("flightDeselected", () => {
selectedFlightId = null;
addLog("flightDeselected", "selection cleared");
});
// ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββ
return {
subscribe(fn: () => void) {
listeners.add(fn);
return () => { listeners.delete(fn); };
},
getState(): DashboardState {
return snapshot;
},
setWeather(condition: WeatherCondition) {
mediator.emit("weatherChanged", { condition, severity: WEATHER_SEVERITY[condition] });
commit();
},
selectFlight(flightId: string) {
const f = flights.find((fl) => fl.id === flightId);
if (f) {
mediator.emit("flightSelected", { flightId: f.id, destination: f.destination, gate: f.gate });
commit();
}
},
deselectFlight() {
mediator.emit("flightDeselected", {});
commit();
},
reset() {
flights = INITIAL_FLIGHTS.map((f) => ({ ...f }));
weather = "clear";
severity = 0;
runway = "full";
selectedFlightId = null;
logs.length = 0;
logId = 0;
commit();
},
};
}
import { useState } from "react";
import { RotateCcw, Terminal, X } from "lucide-react";
import { useDashboard } from "@/hooks/useDashboard";
import { BRAND_COLOR } from "./constants";
import { WeatherPanel } from "./WeatherPanel";
import { FlightListPanel } from "./FlightListPanel";
import { RunwayPanel } from "./RunwayPanel";
import { MediatorLog } from "./MediatorLog";
export function FlightDashboard() {
const {
state, selectedFlight, logScrollRef,
handleWeather, handleSelectFlight, handleReset,
} = useDashboard();
const [logOpen, setLogOpen] = useState(false);
return (
<div className="h-screen flex flex-col bg-slate-100 overflow-hidden">
{/* Header */}
<div className="shrink-0 px-4 py-2.5" style={{ background: BRAND_COLOR }}>
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<img src="dgac-logo.svg" alt="DGAC" className="h-8 w-8 object-contain brightness-0 invert" />
<div>
<h1 className="text-sm font-bold text-white leading-none">DGAC Flight Control</h1>
<p className="text-[10px] text-blue-200/70 mt-0.5">
3 panels, 1 mediator : <code className="text-blue-100/80">createMediator()</code>
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleReset}
className="p-2 text-blue-200/60 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<RotateCcw className="w-4 h-4" />
</button>
<button
onClick={() => setLogOpen(!logOpen)}
className="md:hidden p-2 bg-white/10 rounded-lg relative"
>
{logOpen ? <X className="w-4 h-4 text-white/70" /> : <Terminal className="w-4 h-4 text-white" />}
{!logOpen && !!state.logs.length && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-[8px] font-bold text-white flex items-center justify-center">
{state.logs.length}
</span>
)}
</button>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 min-h-0">
{logOpen && (
<div className="md:hidden h-full p-3">
<MediatorLog logs={state.logs} scrollRef={logScrollRef} />
</div>
)}
<div className={`h-full max-w-6xl mx-auto px-1 sm:px-4 py-1 ${logOpen ? "hidden md:grid" : "flex flex-col md:grid"} md:grid-cols-5 md:gap-4`}>
<div className="md:col-span-3 overflow-y-auto min-h-0 space-y-3 pb-2">
<WeatherPanel
current={state.weather}
severity={state.severity}
destination={selectedFlight?.destination}
onChangeWeather={handleWeather}
/>
<div className="space-y-3">
<FlightListPanel
flights={state.flights}
selectedId={state.selectedFlightId}
onSelect={handleSelectFlight}
/>
<RunwayPanel
capacity={state.runway}
gate={selectedFlight?.gate}
flightId={selectedFlight?.id}
/>
</div>
</div>
<div ref={logScrollRef} className="hidden md:flex md:col-span-2 flex-col overflow-y-auto min-h-0">
<MediatorLog logs={state.logs} />
</div>
</div>
</div>
<div className="shrink-0 py-1.5 text-center text-[10px] text-slate-400">
Panels communicate through <code className="text-slate-500 font-medium">createMediator()</code> : no direct references
</div>
</div>
);
}
Real-World Analogyβ
An air traffic control tower. Planes don't communicate directly with each other: that would be chaos. They all talk to the tower, which coordinates takeoffs, landings, and flight paths. The tower is the mediator.
When to Use Itβ
- Many components need to communicate but shouldn't know about each other
- You want to centralize complex interaction logic
- Adding new components shouldn't require modifying existing ones
- You need a log of all inter-component messages
When NOT to Use Itβ
If two components have a simple parent-child relationship, direct props/callbacks are clearer. Don't route everything through a mediator when a function call would do.
APIβ
- createMediator β Create a typed event hub for decoupled communication