After mastering design patterns, SOLID principles, functional programming, and bundle optimization, it's time to combine this knowledge in practice. In this chapter, we will build an event-driven system with time simulation - a pattern commonly used in games, simulations, and monitoring systems.
Event-driven architecture is an approach where program flow is controlled by events - user actions, sensor signals, time passage, or messages from other systems. Instead of linear code execution, the system reacts to events as they occur.
1// Traditional approach - linear
2function checkPark() {
3 checkFences();
4 feedDinosaurs();
5 updateVisitors();
6 generateReport();
7}
8
9// Event-driven - reactive
10const eventBus = new EventEmitter();
11
12eventBus.on('fence:alarm', handleFenceAlarm);
13eventBus.on('dinosaur:hungry', scheduleFeedingRun);
14eventBus.on('visitor:entered', updateVisitorCount);
15eventBus.on('hour:changed', generateHourlyReport);In the event-driven approach, each component operates independently and reacts only to events that concern it. This leads to loose coupling - one of the key SOLID principles you learned earlier.
At the heart of event-driven architecture is EventEmitter - an implementation of the Observer pattern that we learned in the first lesson of this module.
1class EventEmitter {
2 constructor() {
3 this.listeners = new Map();
4 }
5
6 on(event, callback) {
7 if (!this.listeners.has(event)) {
8 this.listeners.set(event, []);
9 }
10 this.listeners.get(event).push(callback);
11 return this; // Fluent interface
12 }
13
14 off(event, callback) {
15 if (!this.listeners.has(event)) return this;
16 const callbacks = this.listeners.get(event);
17 const index = callbacks.indexOf(callback);
18 if (index > -1) callbacks.splice(index, 1);
19 return this;
20 }
21
22 emit(event, ...args) {
23 if (!this.listeners.has(event)) return false;
24 this.listeners.get(event).forEach(callback => {
25 callback(...args);
26 });
27 return true;
28 }
29
30 once(event, callback) {
31 const wrapper = (...args) => {
32 callback(...args);
33 this.off(event, wrapper);
34 };
35 return this.on(event, wrapper);
36 }
37}The
once method is an elegant example of the Decorator pattern - it wraps the original callback in a wrapper that automatically unregisters itself after the first invocation.In simulation systems, time is a key element. In Jurassic Park, the day/night cycle affects dinosaur behavior, feeding schedules, visiting hours, and security protocols.
1class TimeSimulation extends EventEmitter {
2 constructor() {
3 super();
4 this.currentHour = 6; // Start at 6:00
5 this.day = 1;
6 this.scheduledEvents = [];
7 this.isRunning = false;
8 }
9
10 advanceHour() {
11 this.currentHour++;
12
13 if (this.currentHour >= 24) {
14 this.currentHour = 0;
15 this.day++;
16 this.emit('day:changed', this.day);
17 }
18
19 this.emit('hour:changed', this.currentHour, this.day);
20
21 // Check scheduled events
22 this.checkScheduledEvents();
23
24 // Emit time-of-day events
25 const timeOfDay = this.getTimeOfDay();
26 this.emit('timeOfDay:changed', timeOfDay);
27 }
28
29 getTimeOfDay() {
30 if (this.currentHour >= 6 && this.currentHour < 12) return 'morning';
31 if (this.currentHour >= 12 && this.currentHour < 18) return 'day';
32 if (this.currentHour >= 18 && this.currentHour < 22) return 'evening';
33 return 'night';
34 }
35
36 scheduleEvent(hour, callback, options = {}) {
37 const event = {
38 hour,
39 callback,
40 recurring: options.recurring || false,
41 label: options.label || 'unnamed'
42 };
43 this.scheduledEvents.push(event);
44 return event;
45 }
46
47 checkScheduledEvents() {
48 this.scheduledEvents = this.scheduledEvents.filter(event => {
49 if (event.hour === this.currentHour) {
50 event.callback(this.currentHour, this.day);
51 return event.recurring; // Keep only recurring ones
52 }
53 return true;
54 });
55 }
56}Notice how the
TimeSimulation class inherits from EventEmitter - this is an application of the Open/Closed principle from SOLID. We can extend simulation behavior by adding new listeners without modifying the class itself.In a real system, many events repeat cyclically - feeding dinosaurs, security patrols, status reports.
1const sim = new TimeSimulation();
2
3// Feeding carnivores - daily at 7:00 and 17:00
4sim.scheduleEvent(7, (hour, day) => {
5 console.log(`Day ${day}, ${hour}:00 - Feeding carnivores`);
6}, { recurring: true, label: 'feeding-carnivores' });
7
8sim.scheduleEvent(17, (hour, day) => {
9 console.log(`Day ${day}, ${hour}:00 - Second feeding for carnivores`);
10}, { recurring: true, label: 'feeding-carnivores-2' });
11
12// Security patrols - every hour at night
13for (let h = 22; h <= 23; h++) {
14 sim.scheduleEvent(h, (hour, day) => {
15 console.log(`Day ${day}, ${hour}:00 - Night patrol`);
16 }, { recurring: true, label: `patrol-${h}` });
17}
18
19// Daily report - at 8:00
20sim.scheduleEvent(8, (hour, day) => {
21 console.log(`Day ${day} - Generating daily report`);
22}, { recurring: true, label: 'daily-report' });Event-driven architecture allows different subsystems to react independently to the same change:
1// Lighting system
2sim.on('timeOfDay:changed', (timeOfDay) => {
3 const lightingProfiles = {
4 morning: { brightness: 70, color: 'warm-white' },
5 day: { brightness: 100, color: 'daylight' },
6 evening: { brightness: 40, color: 'amber' },
7 night: { brightness: 10, color: 'red' } // Red so as not to startle dinosaurs
8 };
9 const profile = lightingProfiles[timeOfDay];
10 console.log(`Lighting: ${profile.brightness}% (${profile.color})`);
11});
12
13// Security system
14sim.on('timeOfDay:changed', (timeOfDay) => {
15 if (timeOfDay === 'night') {
16 console.log('Activating night security protocols');
17 console.log('Increasing fence voltage by 20%');
18 console.log('Enabling thermal cameras');
19 } else if (timeOfDay === 'morning') {
20 console.log('Deactivating night protocols');
21 console.log('Restoring standard fence voltage');
22 }
23});
24
25// Visitor system
26sim.on('hour:changed', (hour) => {
27 if (hour === 9) console.log('Park open for visitors');
28 if (hour === 18) console.log('Last visitor admission');
29 if (hour === 20) console.log('Park closed - guest evacuation');
30});Event-driven architecture can be enriched with functional techniques we learned earlier in the module:
1// Pure functions for data transformation
2const calculateFeedingAmount = (species, weight, hour) => {
3 const baseAmount = weight * 0.02; // 2% of body mass
4 const timeMultiplier = hour < 12 ? 1.2 : 0.8; // More in the morning
5 return Math.round(baseAmount * timeMultiplier);
6};
7
8// Function composition for event processing
9const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
10
11const processAlert = pipe(
12 (alert) => ({ ...alert, timestamp: Date.now() }),
13 (alert) => ({ ...alert, severity: alert.level > 7 ? 'critical' : 'warning' }),
14 (alert) => ({ ...alert, notified: true })
15);
16
17// Usage with event system
18sim.on('alert:triggered', (rawAlert) => {
19 const processedAlert = processAlert(rawAlert);
20 console.log(`Alert [${processedAlert.severity}]: ${processedAlert.message}`);
21});We combine the state management pattern (exercise_9_4) with the event system:
1class SimulationState {
2 constructor(simulation) {
3 this.state = {
4 parkStatus: 'operational',
5 activeAlerts: 0,
6 dinosaursCount: 0,
7 visitorsCount: 0,
8 fencesStatus: 'all-operational'
9 };
10 this.history = [];
11
12 // React to simulation events
13 simulation.on('hour:changed', (hour) => {
14 this.updateState({ lastUpdate: hour });
15 });
16
17 simulation.on('alert:triggered', () => {
18 this.updateState({
19 activeAlerts: this.state.activeAlerts + 1
20 });
21 });
22 }
23
24 updateState(changes) {
25 // Immutability - new state object
26 const previousState = { ...this.state };
27 this.state = { ...this.state, ...changes };
28 this.history.push({
29 timestamp: Date.now(),
30 previous: previousState,
31 current: { ...this.state }
32 });
33 }
34
35 getState() {
36 return Object.freeze({ ...this.state });
37 }
38}Event-driven systems are easy to test thanks to loose coupling of components:
1// Time simulation test
2function testTimeSimulation() {
3 const sim = new TimeSimulation();
4 const events = [];
5
6 sim.on('hour:changed', (hour) => events.push({ type: 'hour', hour }));
7 sim.on('day:changed', (day) => events.push({ type: 'day', day }));
8
9 // Simulate 25 hours (going through an entire day)
10 for (let i = 0; i < 25; i++) {
11 sim.advanceHour();
12 }
13
14 console.log(`Recorded events: ${events.length}`);
15 console.log(`Day changes: ${events.filter(e => e.type === 'day').length}`);
16 console.log(`Current day: ${sim.day}, hour: ${sim.currentHour}`);
17}
18
19testTimeSimulation();Event-driven architecture combines many patterns and principles that you learned in this module:
This is preparation for the final project, where you will integrate all these elements into a complete Jurassic Park management system.