Event Hub with a sprinkling of NIH

10 November 2013

I have two things: the need for an event hub in my Typescript application, and a mild case of NIH syndrome.

I say mild because from brief searching around I can't find any existing library that suits my needs so I can either shoehorn the design for my application to fit something else or invest the same time and effort into creating one that fits like a glass slipper.

My Requirements

I want to be able to register for events like this:

  • register('AssetUpload::Queued', asset => this.addToUi(asset))
  • register('AssetUpload:42:UploadComplete', asset => this.handleUploadComplete(asset))
  • register('AssetUpload:42:*', (asset, event) => this.handleAllEventsForAsset(event))
  • register('*', (data, event) => console.log({event: event, data: data}))

This segmented event name structure may not turn out to be the best but it's what I've come up with so far in order to allow arbitrary specificity when subscribing to events. Events should always be triggered as specifically as possible and allow the hub to filter the recipients.

An asterisk should match one or more segments, and an empty segment should match exactly one segment. So:

  • AssetUpload:42:Queued would trigger both AssetUpload:*:Queued and AssetUpload::Queued,
  • Asset:42:Upload:Queued would only trigger Asset:*:Queued but not Asset::Queued.

Implementation

I'm implementing this naively for the time being as my project is still in its infancy and performance/robustness will be addressed once some core functionality is in place.

When registering for an event, the event name will be turned into a regular expression and pushed into an array. When an event is triggered the loop is iterated, and if the regular expression for a registered event matches, the associated handler function is called.

There are two replacements that need to be made on the event name to turn it into the correct regular expression. An asterisk must be turned into the greedy .* operator, and an empty segment must have [^:]* inserted into it to allow matching of a single segment.

Feeding the above into the keyboard results in the below snippet.

module mc {
    export class EventHub {
        private handlers: Array<EventHandler>;

        constructor() {
            this.handlers = [];
        }

        public register(event: string, handler: (data: any) => void ): void {
            this.handlers.push(new EventHandler(event, handler));
        }

        public trigger(event: string, data?: any): void {
            for (var i = 0; i < this.handlers.length; ++i) {
                if (this.handlers[i].regex.test(event)) {
                    this.handlers[i].handler(data, event);
                }
            }
        }
    }

    class EventHandler {
        public handler: (data: any, event: string) => void;
        public event: string;
        public regex: RegExp;

        constructor(event: string, handler: (data: any, event: string) => void ) {
            this.handler = handler;
            this.event = event;
            this.regex = new RegExp(event.replace('*', '.*').replace('::', ':[^:]*:'));
        }
    }
}

Shortcomings

This is far from a complete implementation, there is no way to unregister from an event and it's probably not the most performant, but it fits my needs for the time being. I shall revisit this when it becomes a problem.