Package detail

postal

postaljs109.9k2.0.6

Pub/Sub library providing wildcard subscriptions, complex message handling, etc. Works server and client-side.

pub/sub, pub, sub, messaging

readme

Postal.js

Build Status

Version 2.0.6 (MIT)

See the changelog for information on if the current version of postal has breaking changes compared to any older version(s) you might be using. Version 0.11+ removed the dependency on ConduitJS and significantly improved publishing performance. Version 1.0+ added an optional embedded-and-customized-lodash build.

What is it?

Postal.js is an in-memory message bus - very loosely inspired by AMQP - written in JavaScript. Postal.js runs in the browser, or on the server using node.js. It takes the familiar "eventing-style" paradigm (of which most JavaScript developers are familiar) and extends it by providing "broker" and subscriber implementations which are more sophisticated than what you typically find in simple event emitting/aggregation.

Usage - at a glance

If you want to subscribe to a message, you tell postal what channel and topic to listen on (channel is optional, as postal provides a default one if you don't), and a callback to be invoked when a message arrives:

var subscription = postal.subscribe({
    channel: "orders",
    topic: "item.add",
    callback: function(data, envelope) {
        // `data` is the data published by the publisher.
        // `envelope` is a wrapper around the data & contains
        // metadata about the message like the channel, topic,
        // timestamp and any other data which might have been
        // added by the sender.
    }
});

The publisher might do something similar to this:

postal.publish({
    channel: "orders",
    topic: "item.add",
    data: {
        sku: "AZDTF4346",
        qty: 21
    }
});

Channels? WAT?

A channel is a logical partition of topics. Conceptually, it's like a dedicated highway for a specific set of communication. At first glance it might seem like that's overkill for an environment that runs in an event loop, but it actually proves to be quite useful. Every library has architectural opinions that it either imposes or nudges you toward. Channel-oriented messaging nudges you to separate your communication by bounded context, and enables the kind of fine-tuned visibility you need into the interactions between components as your application grows.

While the above code snippets work just fine, it's possible to get a more terse API if you want to hang onto an ChannelDefinition instance - which is really a convenience wrapper around publishing and subscribing on a specific channel (instead of having to specify it each time):

var channel = postal.channel("orders");

var subscription = channel.subscribe("item.add", function(data, envelope) {
    /*do stuff with data */
});

channel.publish("item.add", {
    sku: "AZDTF4346",
    qty: 21
});

But Wait - How's This Different Than {Insert "X" Eventing Library Here}?

  • postal is not an event emitter - it's not meant to be mixed into an instance. Instead, it's a stand alone "broker" – a message bus.
  • postal uses an envelope to pass messages. This means you get a consistent method signature in ALL of your subscriber callbacks. Most eventing libs take what I call the "0-n args approach", and what gets passed to the subscriber in those libs is solely at the mercy/whim of the developer that wrote the code emitting the event.
  • Most "event aggregator" libs are single channel - which can lead to event name collision, and reduce the performance of matching an event to the correct subscribers. postal is multi-channel.
  • postal's design strongly discourages publishing behavior (functions/methods)! This is intentional. In an observer-subject scenario, it's common to pass callbacks on the event data. This is an anti-pattern when it comes to messaging (and I'd argue is often an anti-pattern even in Observer pattern scenarios). A postal envelope should be serializable and then de-serializable with no loss of fidelity.
  • postal can work across frame and web worker boundaries (by utilizing the postal.federation and postal.xframe plugins).
  • postal's built-in topic logic supports hierarchical wildcard topic bindings - supporting the same logic as topic bindings in the AMQP spec. And if you don't like that approach, you can easily provide your own bindings resolver.

Why would I use it?

Using a local message bus can enable to you de-couple your web application's components in a way not possible with other 'eventing' approaches. In addition, strategically adopting messaging at the 'seams' of your application (e.g. - between modules, at entry/exit points for browser data and storage) can not only help enforce better overall architectural design, but also insulate you from the risks of tightly coupling your application to 3rd party libraries. For example:

  • If you're using a client-side binding framework, and either don't have - or don't like - the request/communication abstractions provided, then grab a library like amplify.js or reqwest. Then, instead of tightly coupling them to your application, have the request success/error callbacks publish messages with the appropriate data and any subscribers you've wired up can handle applying the data to the specific objects/elements they're concerned with.
  • Do you need two view models to communicate, but you don't want them to need to know about each other? Have them subscribe to the topics about which they are interested in receiving messages. From there, whenever a view model needs to alert any listeners of specific data/events, just publish a message to the bus. If the other view model is present, it will receive the notification.
  • Want to wire up your own binding framework? Want to control the number of times subscription callbacks get invoked within a given time frame? Want to keep subscriptions from being fired until after data stops arriving? Want to keep events from being acted upon until the UI event loop is done processing other events? Postal.js gives you the control you need in these kinds of scenarios via the options available on the SubscriptionDefinition object.
  • postal.js is extensible. Plugins like postal.when can be included to provide even more targeted functionality to subscribers. Postal.federation provides the core bits needed to federate postal instances running in different environments (currently the only federation plugin available is postal.xframe for federating between windows in the browser, but more plugins are in the works). These - and more - are all things Postal can do for you.

Philosophy

Dang - you've read this far! AMAZING! If I had postal t-shirts, I'd send you one!

These four concepts are central to postal:

  • channels should be provided to allow for logical partitioning of "topics"
  • topics should be hierarchical and allow plain string or wildcard bindings
  • messages should include envelope metadata
  • subscriber callbacks should get a consistent method signature

Most eventing libraries focus on providing Observer Pattern utilities to an instance (i.e. - creating an event emitter), OR they take the idea of an event emitter and turn it into an event aggregator (so a single channel, stand alone emitter that acts as a go-between for publishers and subscribers). I'm a big fan of the Observer Pattern, but its downside is that it requires a direct reference to the subject in order to listen to events. This can become a big source of tight coupling in your app, and once you go down that road, your abstractions tend to leak, if not hemorrhage.

postal is not intended to replace Observer pattern scenarios. Inside a module, where it makes sense for an observer to have a direct reference to the subject, by all means, use the Observer pattern. However - when it comes to inter-module communication (view-to-view, for example), inter-library communication, cross frame communication, or even hierarchical state change notifications with libraries like ReactJS, postal is the glue to help your components communicate without glueing them with tight coupling.

Hierarchical Topics

In my experience, seeing publish and subscribe calls all over application logic is usually a strong code smell. Ideally, the majority of message-bus integration should be concealed within application infrastructure. Having a hierarchical-wildcard-bindable topic system makes it very easy to keep things concise (especially subscribe calls!). For example, if you have a module that needs to listen to every message published on the ShoppingCart channel, you'd simply subscribe to "#", and never have to worry about additional subscribes on that channel again - even if you add new messages in the future. If you need to capture all messages with ".validation" at the end of the topic, you'd simply subscribe to "#.validation". If you needed to target all messages with topics that started with "Customer.", ended with ".validation" and had only one period-delimited segment in between, you'd subscribe to "Customer.*.validation" (thus your subscription would capture Customer.address.validation and Customer.email.validation").

More on How to Use It

Here are four examples of using Postal. All of these examples - AND MORE! - can run live here. Be sure to check out the wiki for API documentation and conceptual walk-throughs.

// This gets you a handle to the default postal channel...
// For grins, you can get a named channel instead like this:
// var channel = postal.channel( "DoctorWho" );
var channel = postal.channel();

// subscribe to 'name.change' topics
var subscription = channel.subscribe( "name.change", function ( data ) {
    $( "#example1" ).html( "Name: " + data.name );
} );

// And someone publishes a name change:
channel.publish( "name.change", { name : "Dr. Who" } );

// To unsubscribe, you:
subscription.unsubscribe();

// postal also provides a top-level ability to subscribe/publish
// used primarily when you don't need to hang onto a channel instance:
var anotherSub = postal.subscribe({
    channel  : "MyChannel",
    topic    : "name.change",
    callback : function(data, envelope) {
        $( "#example1" ).html( "Name: " + data.name );
    }
});

postal.publish({
    channel : "MyChannel",
    topic   : "name.change",
    data    : {
        name : "Dr. Who"
    }
});

Subscribing to a wildcard topic using *

The * symbol represents "one word" in a topic (i.e - the text between two periods of a topic). By subscribing to "*.changed", the binding will match name.changed & location.changed but not changed.companion.

var chgSubscription = channel.subscribe( "*.changed", function ( data ) {
    $( "<li>" + data.type + " changed: " + data.value + "</li>" ).appendTo( "#example2" );
} );
channel.publish( "name.changed",     { type : "Name",     value : "John Smith" } );
channel.publish( "location.changed", { type : "Location", value : "Early 20th Century England" } );
chgSubscription.unsubscribe();

Subscribing to a wildcard topic using #

The # symbol represents 0-n number of characters/words in a topic string. By subscribing to "DrWho.#.Changed", the binding will match DrWho.NinthDoctor.Companion.Changed & DrWho.Location.Changed but not Changed.

var starSubscription = channel.subscribe( "DrWho.#.Changed", function ( data ) {
    $( "<li>" + data.type + " Changed: " + data.value + "</li>" ).appendTo( "#example3" );
} );
channel.publish( "DrWho.NinthDoctor.Companion.Changed", { type : "Companion Name", value : "Rose"   } );
channel.publish( "DrWho.TenthDoctor.Companion.Changed", { type : "Companion Name", value : "Martha" } );
channel.publish( "DrWho.Eleventh.Companion.Changed",    { type : "Companion Name", value : "Amy"    } );
channel.publish( "DrWho.Location.Changed",              { type : "Location",       value : "The Library" } );
channel.publish( "TheMaster.DrumBeat.Changed",          { type : "DrumBeat",       value : "This won't trigger any subscriptions" } );
channel.publish( "Changed",                             { type : "Useless",        value : "This won't trigger any subscriptions either" } );
starSubscription.unsubscribe();

Applying distinctUntilChanged to a subscription

var dupChannel = postal.channel( "Blink" ),
    dupSubscription = dupChannel.subscribe( "WeepingAngel.#", function( data ) {
                          $( '<li>' + data.value + '</li>' ).appendTo( "#example4" );
                      }).distinctUntilChanged();
// demonstrating multiple channels per topic being used
// You can do it this way if you like, but the example above has nicer syntax (and *much* less overhead)
dupChannel.publish( "WeepingAngel.DontBlink", { value:"Don't Blink" } );
dupChannel.publish( "WeepingAngel.DontBlink", { value:"Don't Blink" } );
dupChannel.publish( "WeepingAngel.DontEvenBlink", { value:"Don't Even Blink" } );
dupChannel.publish( "WeepingAngel.DontBlink", { value:"Don't Close Your Eyes" } );
dupChannel.publish( "WeepingAngel.DontBlink", { value:"Don't Blink" } );
dupChannel.publish( "WeepingAngel.DontBlink", { value:"Don't Blink" } );
dupSubscription.unsubscribe();

More References

Please visit the postal.js wiki for API documentation, discussion of concepts and links to blogs/articles on postal.js.

How can I extend it?

There are four main ways you can extend Postal:

  • Write a plugin. Need more complex behavior that the built-in SubscriptionDefinition doesn't offer? Write a plugin that you can attach to the global postal object. See postal.when for an example of how to do this. You can also write plugins that extend the ChannelDefinition and SubscriptionDefinition prototypes - see postal.request-response for an example of this.
  • Write a custom federation plugin, to federate instances of postal across a transport of your choice.
  • You can also change how the bindingResolver matches subscriptions to message topics being published. You may not care for the AMQP-style bindings functionality. No problem! Write your own resolver object that implements a compare and reset method and swap the core version out with your implementation by calling: postal.configuration.resolver = myWayBetterResolver.

It's also possible to extend the monitoring of messages passing through Postal by adding a "wire tap". A wire tap is a callback that will get invoked for any published message (even if no actual subscriptions would bind to the message's topic). Wire taps should not be used in lieu of an actual subscription - but instead should be used for diagnostics, logging, forwarding (to a websocket publisher or a local storage wrapper, for example) or other concerns that fall along those lines. This repository used to include a console logging wiretap called postal.diagnostics.js - you can now find it here in it's own repo. This diagnostics wiretap can be configured with filters to limit the firehose of message data to specific channels/topics and more.

Build, Dependencies, etc.

  • postal depends on lodash.js
    • The standard postal build output (lib/postal.js) is what you would normally use if you're already using lodash in your app.
    • The lib/postal.lodash.js build output might be of interest to you if these things are true:
      • You're using webpack, browserify or another bundler/loader capable of loading CommonJS modules.
      • You only need the specific bits of lodash used by postal and don't need the full lib.
  • postal uses gulp.js for building, running tests and examples.
    • To build
      • run npm install (to install all deps)
      • run bower install (yep, we're using at least one thing only found on bower in the local project runner)
      • run npm run build (this is just an alias for gulp at the moment, which you can also use) - then check the lib folder for the output
    • To run tests & examples
      • Tests are node-based: npm test (or npm run test-lodash to test the custom lodash build output)
      • To run browser-based examples:
        • run npm start
        • navigate in your browser to http://localhost:3080/
        • if you want to see test coverage or plato reports be sure to run npm run coverage and gulp report (respectively) in order to generate them, as they are not stored with the repo.

Can I contribute?

Please - by all means! While I hope the API is relatively stable, I'm open to pull requests. (Hint - if you want a feature implemented, a pull request gives it a much higher probability of being included than simply asking me.) As I said, pull requests are most certainly welcome - but please include tests for your additions. Otherwise, it will disappear into the ether.

changelog

v2.x

v2.0.6

  • Updated lodash to 4.17.21
  • Addressed formatting issues in the README
  • Went a bit crazy in the .npmignore (even though this package uses the files option in the package.json)
  • Added an nvmrc to run local examples in node 11, since the examples are broken in later versions (does not affect postal's ability to run in later node versions!)

    v2.0.5

  • Fixed caching when resolverNoCache is set to true
  • Allowed resolverNoCache config option to be passed to envelope headers

v2.0.4

  • Conditionally calling .noConflict (only if previous global. was truthy and not equal to postal's lodash version)

v2.0.3

  • Fixed lodash isEqual file name casing.

v2.0.2

  • Thanks to @jcreamer898:
    • Fixed lodash paths, removed unnecesary _.noConflict() call.
    • Added travis.yml.

v2.0.1

  • Added call to lodash's noConflict.

v2.0.0

  • Merged #151 (breaking change, requires function.prototype.bind polyfill in older browsers)
  • Removed deprecated SubscriptionDefinition methods (breaking change)
    • Removed method names: withConstraint, withConstraints, withContext, withDebounce, withDelay, withThrottle
    • Correct/current method names: constraint, constraints, context, debounce, delay, throttle

v1.x

v1.0.11

  • Fixed even more issues I missed with lodash 4
  • Made note-to-self to be extra careful cutting new tags while sick.

v1.0.10

  • Fixed issue where removed lodash alias was still in use
  • Fixed issue this context issue in postal.subscribe

v1.0.9

  • Merged #148 - Updated to lodash 4.x
  • Merged #128 - Remove unused bower.json version prop

v1.0.8

  • Fixed #136, where global was undefined when setting prevPostal for noConflict situations.

v1.0.7

  • Included @derickbailey's awesome logo addition!
  • Updated gulp build setup to run formatting on src and lib.
  • Added a throw when ChannelDefinition.prototype.publish is not passed a valid first argument.

v1.0.6

  • Fixed issue where JSCS's formatting fix put a line break before catch on the SubscriptionDefinition prototype.
  • Added additional comment directive removal (covering jshint, jscs & istanbul).

v1.0.5

  • Fixed issue (referred to in #124) related to the custom lodash build not pulling in necessary behavior for _.each.
  • Added deprecation warnings to istanbul ignore.

v1.0.4

  • Fixed issue where a subscriber lookup cache (postal's internal cache) was failing to update when new subscribers were added after a publish matching the cache had occurred.

v1.0.3

  • Fixed memory leak issue referred to here. Postal will not place subscriptions in the lookup cache if the resolverNoCache header is present on the published envelope.

v1.0.2

  • Updated lodash dependency to 3.x.

v1.0.0

  • 3.5 years in the making, 1.0 finally arrives! :-)
  • Updated lodash dependency to ~3.1.0
  • Customized lodash build option added (lib/postal.lodash.js) - containing only the lodash bits needed for postal (special thanks to @jdalton for making that happen!).
  • Updated gulpfile to produce standard and custom-lodash builds.
  • Updated package.json scripts to allow for testing both standard and lodash builds
  • Added an .editorconfig file to normalize indentation and whitespace concerns.

v0.x

v0.12.4

  • Added support for publish metadata callback (thanks @arobson).
  • Removing minified output from bower.json's main array (thanks @iam-merlin).

v0.12.3

  • Merged in @efurmantt's PR to support toggling resolver cache on and off.

v0.12.2

  • Fixed bug with resolverNoCache option where matches would fail if caching was disabled.

v0.12.1

  • Added support for an envelope header value called resolverNoCache. If present in enveloper.headers and set to true, it will prevent the resolver from caching topic/binding matches for that message instance.

v0.12.0

  • Added the purge method to the default bindings resolver
  • Added the autoCompactResolver option to postal.configuration - it can be set to true (which auto-compacts the resolver cache on every unsubscribe, false (the default) which never automatically compacts the resolver cache or set to an integer > 0, which will auto-compact the resolver cache ever n number of unsubscribes (so setting it to 5 will auto-compact every 5th unsubscribe). "Auto compacting" basically purges any resolver comparison results that do not have subscribers active on those topics (i.e. - nothing it listening to those topics, don't keep the cached comparison results any more).
  • Added the cacheKeyDelimiter option to postal.configuration, which defaults to the pipe (|) symbol. This is primarily to give anyone implementing their own resolver a different way to delimit topics and bindings when they're using to compose a resolver cache key.
  • Added a third argument to the resolver.compare method, which allows you to pass an options object to take into consideration while performing the comparison between topic and binding. Currently, the only supported option is preventCache - which tells the resolver to not cache the result of the comparison.

v0.11.2

  • Two ES5 .bind calls snuck in - we're not officially on ES5 syntax yet (but will be soon). Converting those to use lodash's _.bind call for now.

v0.11.1

  • Fixing an npm publishing goof, which requires a version bump. :-(

v0.11.0

  • ConduitJS is no longer a dependency.
  • invokeSubscriber has been added to the SubscriptionDefinition prototype. This method is called during publish cycles. The postal.publish method no longer does the heavy lifting of determining if a subscriber callback should be invoked, the subscriber now handles that via this new method.
  • The SubscriptionDefinition prototype methods withContext, withThrottle, withDebounce, withDelay, withConstraint, withConstraints have been deprecated and replaced with context, throttle, debounce, delay, constraint and constraints (respectively). They will continue to work in v0.11, but will warn of the deprecation.
  • postal has been optimized for publishing (subscriptions matched to a topic via the resolver are cached).

v0.10.3

  • Wiretaps now get a third argument, the nesting (or publishDepth) argument, which is a number to indicate the 'nested' publish depth for the message being passed to the wiretap. Thanks to @avanderhoorn for this addition. :smile:

v0.10.2

  • Empty topic subscriptions arrays (on postal.subscriptions.whateverChannel) will be removed during unsubscription if the subscriber being removed is the last subscriber.
  • Empty channel objects (on postal.susbcriptions) will be removed during unsubscription if no topic binding properties exist on the channel any longer.
  • Special thanks to @sergiopereiraTT for adding these features. :smile:

v0.10.1

  • Apparently IE 8 doesn't allow "catch" to be used as a method/prop name, unless you utilize bracket notation. (Seriously - With IE6 now a distant memory, I long for the day that IE 8 is dead.) @swaff was kind enough to catch this and submit a patch to take care of it.

v0.10.0

  • (Breaking) Removed the "basic" build of postal. The conclusion was the best (and least confusing) option was to focus on a customized build of lodash - rather than risk fragmentation of postal's features...
  • Added logError and catch to the SubscriptionDefinition courtesy of @arobson.

v0.9.1

  • Replaced underscore dependency with lodash. (You can still use underscore if you need to - but you'll have to replace the lib's references to "lodash" with "underscore")
  • ConduitJS has been an embedded dependency since v0.9.0. I've promoted it to an external dependency because it's clear that Conduit will be useful in add-ons as well. No need to force consumers of postal and its add-ons to double the Conduit line count.

v0.9.0

  • Merged localBus with postal namespace. Postal originally supported the idea of swappable bus implementations. This gold plating has been ripped out finally.
  • Added a noConflict method to allow for side by side instances of different postal versions (for testing/benchmarking).
  • Refactored SubscriptionDefinition significantly to allow for ConduitJS integration into the subscription callback method as well as the ChannelDefinition publish method.
  • Top-level postal.unsubscribe call has been added.
  • Top-level postal.unsubscribeFor call has been added.

Breaking Changes

  • Removed postal.utils namespace. Any methods under utils are now under postal itself.
  • Changed signature of getSubscribersFor to take either an options object or a predicate function.
  • The CommonJS wrapper no longer provides a factory function that has to be invoked. Instead it now simply exports postal itself.
  • Subscriptions are now stored under postal.subscriptions.
  • postal now produces two different builds: a full and minimal build. The minimal build lacks ConduitJS as an embedded dependency, and the only SubscriptionDefinition prototype methods in the minimal build are subscribe, unsubscribe, and withContext. The minimal build is relevant if you do not need to additional features (like defer, withDebounce, etc.) and lib size is a concern.
  • postal.publish and ChannelDefinition.prototype.publish no longer return the envelope.