We use cookies to enhance your experience on the site
CodeWorlds

Event-driven architecture and time systems in JavaScript

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.

What is event-driven architecture?

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.

The EventEmitter pattern

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.

Time simulation - day/night cycle

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.

Scheduling recurring events

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' });

Reacting to times of day

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});

Combining with functional programming

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});

Managing simulation state

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}

Testing event-driven systems

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();

Summary

Event-driven architecture combines many patterns and principles that you learned in this module:

  1. Observer (exercise 9_1) - foundation of the event system
  2. SOLID (exercise 9_2) - loose coupling, openness to extensions
  3. Functional programming (exercise 9_3) - pure functions for event processing
  4. State management (exercise 9_4) - reactive state updates
  5. Optimization (exercise 9_5-9_7) - efficient processing of large numbers of events

This is preparation for the final project, where you will integrate all these elements into a complete Jurassic Park management system.

Go to CodeWorlds