Skip to main content

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.

Code
/**
* 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();
},
};
}
Result

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