topical

1.1.1 • Public • Published

Topical.js

NPM version Build Status Coverage Status Dependencies

JavaScript PubSub library.

Quite a few pubsub libraries already exist. As is the usual case, no library suited either my needs or my tastes.

Some libraries force you to adopt a particular data format when sending messages between decoupled components. Others proved limited and/or buggy.

In general, the publish-subscribe pattern is not particularly difficult to implement. Simply expose a method to register subscribers, keep a mapping table of publishers and their respective subscribers, and route messages as they arrive to the appropriate consumers.

The novelty of this module is fourfold:

  1. Granularity. The library exposes an API which allows subscribing to a particular topic, subscribing to a topic filter, and/or subscribing to global broadcasts.
  2. Meta-information. The exposed broker is an event-emitter and continuously emits events about its comings and goings. In a dynamic system, this allows a straightforward means of monitoring messaging topology.
  3. Minimal assumptions. Few assumptions are made about transmitted data. The only hard requirement is that, when publishing or broadcasting, only one data argument is provided. To publish data involving multiple variables, simply bundle all variables into an object.
  4. Flexilibity. While exposing a singleton broker, the library also provides a method for dynamically creating new brokers. This is convenient when wanting separate brokers for isolated components.

Irrespective of novelty, the principle aim of this library is to facilitate decoupled application architecture. While tight integration can be beneficial, a loosely coupled architecture enables easier testing, more modular design, and small components which do one thing well. Hopefully this library helps in this regard.

Installation

$ npm install topical

For use in the browser, use browserify.

Usage

To use the module,

var topical = require( 'topical' );

The module exports both a singleton and a constructor. The singleton is convenient for a shared application broker. To create several distinct brokers, use the constructor.

var createBroker = topical.ctor;
 
// Create a new broker:
var broker = createBroker();

Brokers have the following methods...

topical.topics()

Returns the list of topics.

var topics = topical.topics();
 
console.log( topics );
// returns [...]

topical.add( topic[, options] )

Registers a new topic with the broker. A topic is configurable and has the following options:

  • max: integer value which specifies the maximum number of subscribers
    • default: Number.MAX_VALUE
  • duplicates: boolean value indicating whether duplicate subscribers are permitted
    • default: false

To register a new topic,

var opts = {
    'max': 10,
    'duplicates': true
};
 
topical.add( 'beep', opts );
 
console.log( topical.topics() );
// returns [ 'beep' ]

topical.remove( topic )

Removes a topic and all associated subscribers. The topic may be either a string or a regular expression. For regular expressions, any topics matching the regular expression are removed.

topical
    .add( 'beep' )
    .remove( 'beep' );
 
console.log( topical.topics() );
// returns []
 
topical
    .add( 'beep' )
    .add( 'boop' )
    .add( 'foo' )
    .remove( /^b.+p$/ );
 
console.log( topical.topics() );
// returns [ 'foo' ]

topical.list( clbk )

Subscribes a callback to public broadcasts. Public broadcasts are not bound to a particular topic; instead, they are dispatched to all publicly listed subscribers (akin to opening a phonebook and calling every publicly listed number).

topical.list( foo );
 
function foo( event ) {
    console.log( event );
}

Note: by default, subscribers are unlisted (becoming listed is opt-in:)).

topical.unlist( clbk )

Unsubscribes a callback from public broadcasts.

topical.unlist( foo );

Note: this does not affect a callback serving as a subscriber to particular topics.

topical.subscribe( topic, clbk )

Subscribes a callback to a topic. The topic may be either a string or a regular expression. For regular expressions, a callback will be subscribed to any topics matching the regular expression.

topical
    .add( 'beep' )
    .add( 'biff' )
    .add( 'bop' )
    .add( 'woot' );
 
// Subscribe to a single topic:
topical.subscribe( 'beep', foo );
 
// Subscribe to any topics matching a pattern:
topical.subscribe( /^b.+p$/, bar );
// ...subscribes `bar` to `beep` and `bop` topics
 
function foo( event ) {
    console.log( event );
}
 
function bar( event ) {
    console.log( event );
}

Note: a topic must exist before attempting to subscribe. If a topic does not exist, an error event is emitted (see below).

Additionally, if a topic is oversubscribed, i.e., reached its maximum subscriber limit,, an error event is emitted.

topical.unsubscribe( topic, clbk )

Unsubscribes a callback from a topic. The topic may be either a string or a regular expression. For regular expressions, a callback will be unsubscribed from any topics matching the regular expression.

topical
    .add( 'beep' )
    .add( 'biff' )
    .add( 'bop' )
    .add( 'woot' );
 
// Subscribe to a single topic:
topical.subscribe( 'beep', foo );
 
// Subscribe to any topics matching a pattern:
topical.subscribe( /^b.+/, bar );
// ...subscribes `bar` to `beep`, `bop`, and `biff` topics
 
// Unsubscribe from a single topic:
topical.unsubscribe( 'beep', foo );
 
// Unsubscribe from any topics matching a pattern:
topical.unsubscribe( /^b.+p/, bar );
// ...unsubscribes `bar` from `beep` and `bop`
 
function foo( event ) {
    console.log( event );
}
 
function bar( event ) {
    console.log( event );
}

topical.once( topic, clbk )

Subscribes a callback to a topic and unsubscribes the callback after its first invocation. This is useful, for example, when you need to perform some initialization the first time an event is published, but not after.

topical
    .add( 'beep' )
    .once( 'beep', foo )
    .publish( 'beep', 'boop' )
    .publish( 'beep', 'bap' );
 
function foo( event ) {
    console.log( event );
    // returns only 'boop'
}

topical.publish( topic, value )

Publishes a value to a specified topic.

topical
    .add( 'beep' )
    .subscribe( 'beep', foo )
    .publish( 'beep', 'boop' );
 
function foo( event ) {
    console.log( event );
    // returns `boop`
}

topical.broadcast( value )

Broadcasts a value to all publicly listed subscribers.

topical
    .list( foo )
    .list( bar )
    .broadcast( 'Beep' );
 
function foo( event ) {
    console.log( event );
    // returns 'Beep'
}
function bar( event ) {
    console.log( event );
    // returns 'Beep'
}

Events

All events emit an object (event) with topic and data properties. If a property is not relevant to the event, the property value is null.

Note: the data value type is specific to the event. For example, for error events, the data value is a string. For subscribe/unsubscribe events, the data value is a function. See the appropriate event to determine the return value type.

'add'

Emitted when a new topic is added to the broker.

topical.on( 'add', function onAdd( event ) {
    console.log( 'Added topic: %s.', event.topic );
});

'remove'

Emitted when a topic is removed from the broker.

topical.on( 'remove', function onRemove( event ) {
    console.log( 'Removed topic: %s.', event.topic );
});

'list'

Emitted when a subscriber becomes publicly listed (i.e., open to receiving public broadcasts).

topical.on( 'list', function onList( event ) {
    var subscriber = event.data;
 
    // Send the subscriber a personalized welcome message:
    subscriber( 'Hello, new subscriber!' );
 
    // Announce the newcomer to everyone:
    topical.broadcast( 'Hello, everybody! Please welcome the new subscriber!' );
});
 
topical.list( foo );
 
function foo( event ) {
    console.log( event );
}

'unlist'

Emitted when a subscriber unsubscribes from public broadcasts.

topical.on( 'unlist', function onUnlist( event ) {
    var unsubscriber = event.data;
 
    // Spam the subscriber one more time:
    unsubscriber( 'So sorry to see you leave...;(' );
});
 
topical
    .list( foo )
    .unlist( foo );
 
function foo( event ) {
    console.log( event );
}

'subscribe'

Emitted when a topic has a new subscriber.

topical.on( 'subscribe', function onSub( event ) {
    var clbk = event.data;
    clbk( 'Welcome to topic %s!', event.topic );
});
 
topical
    .add( 'beep' )
    .subscribe( 'beep', onBeep );
 
function onBeep( event ){
    console.log( event );
}

'unsubscribe'

Emitted when a subscriber unsubscribes from a topic.

topical.on( 'unsubscribe', function onUnsub( event ) {
    var clbk = event.data;
    clbk( 'Goodbye from topic %s!', event.topic );
});
 
topical
    .add( 'beep' )
    .subscribe( 'beep', foo )
    .unsubscribe( 'beep', foo );
 
function foo( event ){
    console.log( event );
}

'publish'

Emitted when an event is published to a topic.

topical.on( 'publish', function onPublish( event ) {
    console.log( 'New event published to topic %s...', event.topic );
});
 
topical
    .add( 'beep' )
    .subscribe( 'beep', function(){} )
    .publish( 'beep', 'boop' );

Note: in line with the NSA ;), only meta data is revealed, not the message contents. Sorry...no eavesdropping on published content.

'broadcast'

Emitted when a broadcast is made to all publicly listed subscribers.

topical.on( 'broadcast', function onBroadcast() {
    console.log( '...another broadcast...' );
});
 
topical
    .list( function(){} )
    .broadcast( 'Beep' );

'error'

Emitted whenever an error is encountered.

  • topic is oversubscribed (maximum number of subscribers exceeded)
  • attempting to subscribe to an unregistered topic
topical.on( 'error', function onError( event ) {
    console.log( event );
});
 
topical.subscribe( 'unknown_topic', function(){} );

Notes

When publishing/broadcasting messages, the value type is left to the user given her particular use case. Internally, topical uses a simple object with two fields: topic and data, where data may assume any value type. What value type you decide to use is up to you.

Examples

Consider the following toy example. First, suppose we have the following application components...

Component 1:

var topical = require( 'topical' );
 
module.exports = function component() {
    topical.subscribe( /^b.+p$/, onEvent );
};
 
function onEvent( event ) {
    console.log( event );
}

Component 2:

var topical = require( 'topical' );
 
module.exports = function component() {
    topical
        .list( onBroadcast )
        .subscribe( /^ba.+/, onEvent );
};
 
function onBroadcast( event ) {
    console.log( event );
}
 
function onEvent( event ) {
    console.log( event );
}

Component 3: collects statistics...

var topical = require( 'topical' );
 
// Initialize a stats object:
var stats = {
    'topics': {},
    'broadcasts': 0,
    'public': 0
};
 
// Listeners...
 
topical.on( 'add', onAdd );
topical.on( 'remove', onRemove );
topical.on( 'subscribe', onSub );
topical.on( 'unsubscribe', onUnsub );
topical.on( 'list', onList );
topical.on( 'unlist', onUnlist );
topical.on( 'publish', onPublish );
topical.on( 'broadcast', onBroadcast );
 
function onAdd( event ) {
    stats.topics[ event.topic ] = {
        'numSubscribers': 0,
        'numPublications': 0
    };
}
function onRemove( event ) {
    delete stats.topics[ event.topic ];
}
function onSub( event ) {
    stats.topics[ event.topic ].numSubscribers += 1;
}
function onUnsub( event ) {
    stats.topics[ event.topic ].numSubscribers -= 1;
}
function onList( event ) {
    stats.public += 1;
}
function onUnlist( event ) {
    stats.public -= 1;
}
function onPublish( event ) {
    stats.topics[ event.topic ].numPublications += 1;
}
function onBroadcast() {
    stats.broadcasts += 1;
}
 
 
// EXPORTS //
 
module.exports = stats;

Let's now create a central broker...

var topical = require( 'topical' ),
    comp1 = require( './component1.js' ),
    comp2 = require( './component2.js' ),
    stats = require( './stats.js' );
 
// Create some topics...
var topics = [
    'beep',
    'boop',
    'bap',
    'baz',
    'foo'
];
 
for ( var i = 0; i < topics.length; i++ ) {
    topical.add( topics[ i ] );
}
 
// Run the components:
comp1();
comp2();
 
// Simulate some chatter...
var vec = [ 0, 0.2, 0.4, 0.6, 0.8 ],
    rand,
    idx,
    topic,
    msg;
 
for ( var j = 0; j < 1000; j++ ) {
    rand = Math.random();
    if ( rand > 0.9 ) {
        topical.broadcast( 'The time is now ' + (new Date()).toString() + '...' );
        continue;
    }
    for ( var k = 0; k < vec.length; k++ ) {
        if ( vec[k] > rand ) {
            idx = k-1;
            break;
        }
    }
    topic = topics[ idx ];
    if ( rand > 0.5 ) {
        msg = 'bebop';
    } else {
        msg = 'woot';
    }
    topical.publish( topic, msg );
}
 
// Output the statistics:
console.log( stats );

To run the example from the top-level application directory,

$ node ./examples/index.js

Tests

Unit

Unit tests use the Mocha test framework with Chai assertions. To run the tests, execute the following command in the top-level application directory:

$ make test

All new feature development should have corresponding unit tests to validate correct functionality.

Test Coverage

This repository uses Istanbul as its code coverage tool. To generate a test coverage report, execute the following command in the top-level application directory:

$ make test-cov

Istanbul creates a ./reports/coverage directory. To access an HTML version of the report,

$ make view-cov

License

MIT license.


Copyright

Copyright © 2014. Athan Reines.

Package Sidebar

Install

npm i topical

Weekly Downloads

4

Version

1.1.1

License

none

Last publish

Collaborators

  • kgryte