Having recently spent some time working in WPF withthe fantastic Composite Application Block (or Prism), I thought I would try bringing one of the more useful features over to JavaScript.
Distributed Events
Distributed events within Prism allow you to define a named event that can be published or subscribed to without the need to have a reference to any other object that depends on the event.
CompositePresentationEvent<string> anEvent; //injected somehow
//publish data anywhere in the application
anEvent.Publish("some event data");
//and consume it anywhere. You just need a reference to the event
anEvent.Subscribe(data => MessageBox.Show(data));
When you are writing a large-scale application this is extremely useful, as it allows very loose coupling between components: if an object is interested in the current selected date then it just subscribes to the DateChanged
event; it doesn’t care where the event is raised from.
Compare this to the traditional event subscription mechanism within .NET - where you need to know the parent object to subscribe - and it is easy to see that this method scales better as a system grows.
Bringing Distributed Events to JavaScript
Given the different natures of web and application development I have not felt too strong a need to pull this functionality over into my JavaScript development, but as I work on larger and more modular single page web applications I am beginning to see a use for them.
So what are the requirements here? I want to be able to
- subscribe to an event without knowing where the change came from
- publish an event from multiple sources
- subscribe to an event in multiple locations
-
acquire and refer to events by name
- in Prism I would generally use a type to refer to an event, but we don’t have types so we’ll use names instead
//publish the current time to the "datechanged" event
$.publish('datechanged', new Date());
//and consume changes to the date anywhere in the application
$.subscribe('datechanged', function(date) {
alert(date);
});
Ideally I would also like to add a couple of extra features:
- Async Invocation - the publisher should (optionally) not have to wait for the subscribers to finish processing the event
- Stateful Events - the subscriber should be able to subscribe after a publication and still receive the details
Implementation
Let’s start off with the subscription, as that will dictate how publication works.
Storing Subscriptions
The first thing is to be able to store multiple subscribers against a named event, and the simplest way to do that is to use an object with array properties:
//create an events object to store name -> event mappings
var events = {},
//and use a function to create singleton event objects as needed
getEvent = function(eventName) {
if (!events[eventName]) {
events[eventName] = {
subscribers: []
};
}
return events[eventName];
};
Here we have a getEvent
method that will check to see if the named event already exists, and will create an empty one if needed.
Note: I’m using an object with a subscribers
array property (instead of just the array itself) so that we can store a bit of metadata alongside the subscriber list later.
The subscribe
method then becomes:
$.subscribe = function(event, callback) {
var subscription;
if (typeof callback === 'function') {
subscription = { callback: callback };
} else {
subscription = callback;
if (!subscription.callback) {
throw 'Callback was not specified on options';
}
}
getEvent(event).subscriptions.push(subscription);
};
This creates a subscription object containing the callback function (again, using an object to allow some metadata storage later), then uses the getEvent
method from earlier to acquire or create the event object and append to the list of subscribers.
We’re allowing this to be called with either a callback function or an options object as the second parameter, so that users that don’t want to specify extra options can use a less verbose syntax.
Publishing Events
Now that we have a list of subscribers attached to each event it is simple enough to write the publish
implementation: all we need to do is find the list of subscribers for the event and invoke the callback on each.
$.publish = window.Utils.publish = function(eventName, data) {
var subscriptions = getEvent(eventName).subscriptions;
for (var i = 0; i < subscriptions.length; i++) {
(function(subscription, data) {
subscription.callback.call(null, data);
})(subscriptions[i], data);
}
};
Supporting Async Events
Quite often, the object sourcing the event doesn’t need to wait on the objects that are listening to events. One of the benefits of loose coupling like this is that producers can ignore the actions of consumers, but at the moment our implementation will cause the publishing object to block until all of the subscribers have finished processing…which could take a while.
To work around this problem we can allow each subscriber to specify whether they want their event to be processed synchronously or asynchronously. With JavaScript being single-threaded (sorta) this means something slightly different to what it would in a WPF application, but the important part is to avoid blocking the publisher.
We can use setTimeout
with a low value (or the minimum value) in our publish implementation to allow the publisher to continue processing uninterrupted, and then for our event handler to execute once it is completed.
if (subscription.async) {
setTimeout(function() {
subscription.callback.call(null, data);
}, 4);
} else {
subscription.callback.call(null, data);
}
Here we are determining whether or not to use async processing based on a flag on the subscription, and as we allowed an options object to be passed into our subscribe function we don’t need any changes there:
$.subscribe('event', {
async: true,
callback: function() {
/*...*/
}
});
You can see the difference in behaviour between sync and async event handlers in this jsFiddle example.
Stateful Events
Perhaps “stateful” isn’t the best name for this concept, but it makes sense to me as the event is aware of it’s last publication, so it has a state.
The use case for this feature is where a subscriber relies on a piece of information being published, but it cannot guarantee that it will subscribe before that publication.
The implementation is simple enough: take a copy of the event payload on each publish…
getEvent(event).lastPayload = data;
…and then serve it up as if it were a new publication whenever a subscriber requests to be ‘stateful’ in the subscribe method…
if (subscription.stateful) {
subscription.callback.call(null, getEvent(event).lastPayload);
}
As with the async implementation, we are already allowing users to specify an options object when they subscribe so there’s no need for any further changes.
//publish some important information
$.publish('eventname', 'some data');
//...wait a while...
//then later, when something else subscribes
$.subscribe('eventname', function(data) {
//this will be called immediately with "some data"
});
Source & Download
I’ve packaged this up alongside my various KnockoutJS utilities (available here) - which have also had a bit of cleaning up in the last week - but as this doesn’t rely on the Knockout library you can grab a separate copy here.