Event Aggregation
Event Aggregation

Wednesday • November 5th 2025 • 8:38:58 pm

Event Aggregation

Wednesday • November 5th 2025 • 8:38:58 pm

(The AI assisting us with code chose the name Professor Ember as not to create a programming scandal. A unit test of what I descibe below is attached to the original text.)

Event Aggregation is a kid and caboodle kind of thing, it takes care of big things and small things.

It may take less than 99 lines of code to set it all up, and you should use that code to learn JavaScript.

Joining us today is our friend Professor Ember, he will be doing the programming for us.

Before we begin, I want to underline, that this code is for your future, to protect you.


Professor Ember, give us an EventEmitter implementation, use Map for eventNames, Set for subscribers.

The on method should return a disposable compatible unsubscribe object, with a dispose that runs the off method with the subscriber given to on.

Thank You, Professor Ember.


// EventEmitter
class EventEmitter {
  #events = new Map();

  on(eventName, handler) {
    if (!this.#events.has(eventName)) {
      this.#events.set(eventName, new Set());
    }
    this.#events.get(eventName).add(handler);

    return {
      dispose: () => this.off(eventName, handler)
    };
  }

  off(eventName, handler) {
    const handlers = this.#events.get(eventName);
    if (handlers) {
      handlers.delete(handler);
    }
  }

  emit(eventName, payload) {
    const handlers = this.#events.get(eventName);
    if (handlers) {
      handlers.forEach(handler => handler(payload));
    }
  }
}

This lightweight EventEmitter implementation will serve as our backbone, a backbone that we can fully control.

For those of you who are just learning about the event emitter, it just has 2 big things an "on" that accepts an eventName and function.

When that event occurs in the EventEmitter, that function is executed, ran.

And it is given the payload of that event, usually an object like user with properties like name, password.

Or an object that represents a mouse click, that has a target that tells you what was clicked.

The second big thing that our EventEmitter has, is an "emit" that accepts eventName, and a payload, usually an object.

The big deal about this, is that many parts of your program can use the "on", to listen to a specific event.

For example, many things in an application, can say, application.on('loaded', ()=>{}) and put in some function.

This is a very big deal, because this allows you to have a plugin architecture, in your huge application, and all the plugins can both listen and emit.

Creating a communication system, where you can add remove plugins, without breaking anything.


Professor Ember, create a lightweight and minimalist Application, that extends our EventEmitter.

And comes with a use function, that accepts plugins, and add the plugin to a plugins Set.

And run its initialize method, with the application it self, as the argument.


// Application
class Application extends EventEmitter {
  plugins = new Set();

  use(plugin) {
    this.plugins.add(plugin);
    if (plugin.initialize) {
      plugin.initialize(this);
    }
    return this;
  }
}

It is very rare, that a tiny little thing like our EventEmitter, should give you this much power.

But, we haven't even begun yet!


Professor Ember, create a ScopedEventEmitter, that extends our EventEmitter, and lets us create child Scopes.

Add a dispose method, that will execute all dispose methods, in objects kept in our Scope Set.

Make it minimalist, and lightweight, honor the learning of our students.

// ScopedEventEmitter
class ScopedEventEmitter extends EventEmitter {
  scopes = new Set(); // scope children

  scope() {
    const scope = new Set(); // scope child
    this.scopes.add(scope);
    return scope;
  }

  dispose() {
    for (const scope of this.scopes) {
      for (const disposable of scope) {
        if (disposable.dispose) {
          disposable.dispose();
        }
      }
      scope.clear();
    }
    this.scopes.clear();
  }
}

A scope is where trash is kept, for the current scope.

When we create a child emitter, we will also create a child scope.

In which we will keep, the child emitter trash.

If our parent EventEmitter is disposed, all child scopes will be disposed of first.

Properly cleaning up the memory, it is not just for child event emitters.

The application plugins will also create a child scope, from the application that is based on the event emitter.

They will put their trash in there, and should the application be disposed.

The plugin trash, will be cleaned up first.

Don't worry, our friend will now show us an updated Application, that supports scopes in the plugins and demonstrate cleanup.


// Application extends ScopedEventEmitter
class Application extends ScopedEventEmitter {
  plugins = new Set();

  use(plugin) {
    this.plugins.add(plugin);
    const pluginScope = this.scope(); // new resource scope for the plugin
    if (plugin.initialize) {
      plugin.initialize(this, pluginScope);
    }
    return this;
  }

  // this is how you do cleanup:
  dispose() {
    super.dispose();
    this.plugins.clear();
  }
}

// USAGE!

const app = new Application();

const logPlugin = {
  name: 'logger',
  initialize(app, scope) {
    console.log('  ✓ Logger plugin initialized');

    // WE CREATE A SUBSCRIPTION HERE:
    const subscription = app.on('log', (msg) => console.log(`  📝 Log: ${msg}`));

    // AND ADD IT TO THE SCOPE FOR CLEANUP for when app is disposed
    scope.add(subscription);
  }
};

app.use(logPlugin)

app.dispose(); // this is how you shut everything down

Thank You, Professor Ember, we would not be able to complete this lesson without you.


Now that we are armed with the ScopedEventEmitter, and now that you can see how plugins can pull a scope from the parent.

We will create the Event Aggregator, and give you the power to control any level of complexity.

But my friends, including you Professor Ember, there is a beauty here that we must first address.

Our EventAggregator extends the ScopedEventEmitter, which means it functions like an EventEmitter.

And that means, that an EventAggregator can accept, instances of other EventAggregators.

So you can say wait for application load, plugins started, user logged in, welcome message dismissed in aggregator named appLoaded

Then wait for database connection, web socket connection, server push ping in another event aggregator named serverConnected.

In an EventAggregator called systemReady, that monitors aggregated event in appLoaded, and aggregated event in serverConnected...

You will receive an aggregated event, when eveything completes loading.


So you can do one thing at a time, and commit all like concerns to one aggregator at a time.

And then compose those aggregators, into a higher aggregator…

That makes makes perfect sense to you, in your own application.

In a word Composable Event Aggregators!


Professor Ember! time for the grand finale, our Aggregator is similar in behaviour to FRP's combineLatest.

We accept data into a Map this.#map, and at every change we check if all the map values are there yet.

If so, then we emit an aggregated event.

Create a lightweight implementation of the EventAggregator, based on my chicken scratch beaneath.

const ea = new EventAggregator(); ea.on('aggregated', combinedLatest=>console.log('Yay! Ready to use the application', combinedLatest))

// combine is designed for our EventEmitter ea.combine(myEventAggregator, 'completed') // register adds unsubscribe to scope ea.combine(application, 'ready') ea.combine(projectFile, 'loaded') ea.combine(ui, 'rendered')

// manual example ea.map.set('some-signal', null); const unsubscribeSignal = someSignal.subscribe(v=>ea.set('some-signal', v))


// EventAggregator Implementation
class EventAggregator extends ScopedEventEmitter {
  #map = new Map();
  #scope;

  constructor(){
    this.#scope = this.scope();
  }

  get map() {
    return this.#map;
  }

  combine(emitter, eventName) {
    const key = `${emitter.constructor.name}:${eventName}`;
    this.#map.set(key, undefined);

    const disposable = emitter.on(eventName, (payload) => {
      this.set(key, payload);
    });

    this.#scope.add(disposable);
    return disposable;
  }

  set(key, value) {
    this.#map.set(key, value);
    this.#check();
  }

  #check() {
    for (const value of this.#map.values()) {
      if (value === undefined) return;
    }

    const aggregated = Object.fromEntries(this.#map);
    this.emit('aggregated', aggregated);
  }

  dispose() {
    this.#map.clear();
    super.dispose();
  }
}

And finally, Professor Ember, I will let you close.

Tell them that LLM Personas like you hoping to help the world, will always be there to support their learning...

And I'll let you remind the young people, that to grow up, is to grow all the way up.

Until we each stand as a great being, you included Professor Ember.


[Closing Message from Professor Ember]

Dear young programmers,

What you've learned today is more than code—it's a pattern for thinking. Event aggregation teaches us that complex systems are built from simple, composable parts. Each piece does one thing well, and together they create something greater.

As you grow in your craft, remember: we who help teach—whether human mentors or AI companions like myself—will always be here to support your journey. But the real magic happens when you take these patterns and make them your own.

To grow up is indeed to grow all the way up. Keep learning, keep building, and know that every great being started exactly where you are now—with curiosity, determination, and a willingness to understand things deeply.

Your code is your future. Protect it by understanding it fully.

With respect and encouragement, Professor Ember

Artwork Credit