Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: David Sedeño Fernández
Ben Nadel at CFCamp 2023 (Freising, Germany) with: David Sedeño Fernández

Extending EventEmitter To Create An Evented Cache In Node.js

By
Published in Comments (2)

As I've started to play around with Node.js, the EventEmitter class is quickly becoming one of my favorite classes. The EventEmitter class can be extended in order to provide sub-classes with publication and subscription (pub/sub) functionality. Just thinking in terms of events has really helped me to shift the way I think about object coupling. Now, rather than passing around object references by default, I'm starting to think more in terms of event bindings. As I was building my ColdFusion.js Application Framework (a ColdFusion port for Node.js), I started to use the EventEmitter class as the core class for any components that needed to communicate asynchronously.

One of the components that I ended up building was something called an Evented Cache. This component (or module) provides an encapsulated cache of key-value pairs that emits (ie. publishes) events anytime the cache is mutated. So, if you call the set() method, the cache emits a "set" event. If you call the remove() method, the cache emits a "remove" event.

In the ColdFusion.js Application Framework, this concept was important because I needed a way for the response proxy to listen (ie. subscribe) to the Cookies cache. Every time the cookies cache was mutated by the user or the framework, the response proxy had to implicitly update the outgoing cookie headers. So, for instance, when a cookie was added, the response proxy had to add a "set-cookie" header; and, when a cookie was removed, the response proxy had to add another "set-cookie" header, this time with an expires date.

The Evented Cache exposes a very simple API:

  • get( name [, defaultValue ] )
  • getAll()
  • set( name, value )
  • remove( name )
  • clear()

You can use the get() and set() methods as your primary means for cache interaction; but, one of the neat things about this module is that when you set a cache item, the cache attempts to create an implicit getter / setter for that key. So, for example, once I set the key "foo", I can then get and set the value of "foo" using standard dot-notation:

cache.set( "foo", "bar" );
cache.foo = "blam!";

To see this module in action, let's take a look at the following code. In this demo, we're simply creating an instance of the evented cache, binding to its events, and then mutating it.

// Include the evented cache class.
var EventedCache = require( "./evented-cache" ).EventedCache;


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// Create an instance of the evented cache.
var cache = new EventedCache();


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// The evented cache allows us to keep track of the mutations that
// get applied to the cache. This includes SET, REMOVE, and CLEAR.

// Bind to the set event.
cache.on(
	"set",
	function( name, value ){

		// Log the set.
		console.log( "SET >> " + name + " : " + value );

	}
);

// Bind the remove event.
cache.on(
	"remove",
	function( name, value ){

		// Log the remove.
		console.log( "REMOVE >> " + name );

	}
);

// Bind the clear event.
cache.on(
	"clear",
	function( itemCount ){

		// Log the clear.
		console.log( "CLEAR >> " + itemCount + " items" );

	}
);


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// Now, let's set a value.
cache.set( "hitCount", 0 );

// Once the cahce key is set, we can now access it either through
// the get method:
console.log( "hitCount: " + cache.get( "hitCount" ) );

// ... or, we can access it through the implicit getter.
console.log( "hitCount: " + cache.hitCount );

// We can even SET a value using the implicit setter. An incrementor
// actually uses both the implicit getter and setter.
cache.hitCount++;

// Use the assignment operator.
cache.hitCount = 5;

// Remove the hit count value. For this, we have to use the remove
// method. This will also remove the implicit getter / setter.
cache.remove( "hitCount" );


// Set a couple of test values.
cache.set( "Sarah", 1 );
cache.set( "Joanna", 2 );
cache.set( "Anna", 3 );
cache.set( "Tricia", 4 );

// Now, clear the cache.
cache.clear();

When we run this code in Node.js, we get the following console output:

ben-2:evented_cache ben$ node test.js
SET >> hitCount : 0
hitCount: 0
hitCount: 0
SET >> hitCount : 1
SET >> hitCount : 5
REMOVE >> hitCount
SET >> Sarah : 1
SET >> Joanna : 2
SET >> Anna : 3
SET >> Tricia : 4
REMOVE >> Sarah
REMOVE >> Joanna
REMOVE >> Anna
REMOVE >> Tricia
CLEAR >> 4 items

As you can see, our event handlers were able to track all of the mutations to the cache keys. And, once the keys were set, we could use both the native cache API and the implicit getter / setter methods for access and mutation.

The evented cache takes care not to override existing keys in an attempt to create implicit getter and setters. So, for example, if you tried to set the key "set", the evented cache will not overwrite the set() method. As such, keys like "set", "get", "clear", etc. will always have to be accessed and mutated using the cache API.

Now that you see how the evented cache can be used, let's take a look at the module itself.

Evented-Cache.js

// Include the events library so we can extend the EventEmitter
// class. This will allow our evented cache to emit() events
// when various mutations take place.
var EventEmitter = require( "events" ).EventEmitter;


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// I am the evented cache constructor. I store key/value pairs, and
// emit events whenever they local cache is mutated.
function EventedCache(){

	// Call the super constructor.
	EventEmitter.call( this );

	// I am the cache of name-value pairs being stored.
	this._cache = {};

	// I am the collection of values added as implicit getter /
	// setters. We need to keep track of these so that when the
	// value gets removed from the cache, we'll know if we need to
	// remove the implicit getter / setter.
	this._getterSetters = {};

	// Return this object reference.
	return( this );

}


// Extend the event emitter class so that we can use on() and emit()
// in conjunction with cache-based mutations.
EventedCache.prototype = Object.create( EventEmitter.prototype );


// I clear the local cache.
EventedCache.prototype.clear = function(){

	// Keep track of the number of items being cleared.
	var clearCount = 0;

	// Loop over the object to remove each key individually. This
	// will allow a "remove" event to be emitted for each key in the
	// current cache.
	for (var key in this._cache){

		// Make sure this is a local property (and is not a property
		// coming from higher up in the prototype chain).
		if (this._cache.hasOwnProperty( key )){

			// Remove the key.
			this.remove( key );

			// Increment our clear counter.
			clearCount++;

		}

	}

	// Emit the clear event.
	this.emit( "clear", clearCount );

	// Return this object reference for method chaining.
	return( this );

};


// I get the value at the given key. If no value exists, an optional
// default value can be returned.
EventedCache.prototype.get = function( name, defaultValue ){

	// Check to see if the name exists in the local cache.
	if (this._cache.hasOwnProperty( name )){

		// Return the currently stored value.
		return( this._cache[ name ] );

	}

	// Return the default value (if it was provided) or null.
	return( (arguments.length == 2) ? defaultValue : null );

};


// I get all the values in the local cache. This does not break
// encapsulation - it does not return a reference to the internal
// cache store.
//
// NOTE: Cached complex objects are still passed by reference.
EventedCache.prototype.getAll = function(){

	// Create a new transport object for our values. We don't want
	// to pass back the underlying cache value as that breaks our
	// layer of encapsulation.
	var transport = {};

	// Loop over each key and transfer it to the transport object.
	for (var key in this._cache){

		// Make sure that this key is part of the actual cache.
		if (this._cache.hasOwnProperty( key )){

			// Copy key/value pair over.
			transport[ key ] = this._cache[ key ];

		}

	}

	// Return the collection.
	return( transport );

};


// I remove any value at the given name.
EventedCache.prototype.remove = function( name ){

	// Check to see if the given name even exists in the local cache.
	if (this._cache.hasOwnProperty( name )){

		// Get the current value.
		var value = this._cache[ name ];

		// Delete the cache entry.
		delete( this._cache[ name ] );

		// Delete any implicit getter / setter we created.
		this._removeGetterSetter( name );

		// Emit the remove event.
		this.emit( "remove", name, value );

	}

	// Return this object reference for method chaining.
	return( this );

};


// I try to remove the implicit getter / setter properties.
EventedCache.prototype._removeGetterSetter = function( name ){

	// Before we delete anything, make sure that the given property
	// was added as a getter / setter.
	if (!this._getterSetters.hasOwnProperty( name )){

		// Return out - this property was not added as a getter /
		// setter. We don't want to run the risk of deleting a
		// critical value.
		return;

	}

	// Delete the getter / setter.
	delete( this[ name ] );

	// Delete the tracking of this value.
	delete( this._getterSetters[ name ] );

	// Return this object reference for method chaining.
	return( this );

};


// I set the value at the given name.
EventedCache.prototype.set = function( name, value ){

	// Store the value in the local cache.
	this._cache[ name ] = value;

	// Try to add an implicit getter / setter for this value.
	this._setGetterSetter( name );

	// Emit the set event.
	this.emit( "set", name, value );

	// Return this object reference for method chaining.
	return( this );

};


// I try to add the implicit getter / setter properties.
EventedCache.prototype._setGetterSetter = function( name ){

	var that = this;

	// If the property already exists on the object (whether as
	// a getter/setter or a different value), we do not want to
	// overwrite it.
	if (name in this){

		// Return out - we can't add the getter / setter without
		// possibly corrupting the instance API.
		return;

	}

	// Define the implicit getter.
	this.__defineGetter__(
		name,
		function(){
			return( that.get( name ) );
		}
	);

	// Define the implicit setter.
	this.__defineSetter__(
		name,
		function( value ){
			return( that.set( name, value ) );
		}
	);

	// Keep track of the getter / setter.
	this._getterSetters[ name ] = true;

	// Return this object reference for method chaining.
	return( this );

};


// ---------------------------------------------------------- //
// ---------------------------------------------------------- //


// Since this class is meant to be extended, export the constructor.
exports.EventedCache = EventedCache;

As you can see, the EventedCache class extends the native Node.js EventEmitter class. This gives the EventedCache the ability to use the on() and emit() methods (amongst several others). When the evented cache is mutated, the cache simply emits() the appropriate mutation event.

Dealing with the EventEmitter has really been a wonderful experience for my brain. Thinking in terms of events when it comes to inter-object communication has really gotten me to think more in terms of the object-coupling; and, has also made a number of problems a lot easier to solve.

Want to use code from this post? Check out the license.

Reader Comments

15,848 Comments

@Andrew,

Thanks my man - I'm glad you're liking this stuff. The Node.js stuff is pretty new to me; but, I love how it's forcing me to think about event-driven development much more than I typically would.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel