Getting Started

Getting arbiter up and running is simple. It can be used as a node_module on the client (using WebPack et al.) or on the server. It can even be downloaded and set into a third_party or vendor directory and used with a high fidelity <script> tag. Try it out in the console (unless you're on github)!

npm install promissory-arbiter

Here is a quick example to get you started. If you want to see more advanced examples keep on reading. Take a look at the documentation site.

var Arbiter = require('promissory-arbiter');
var log = function(data, topic) {
  console.log(topic, data);
};

Arbiter.subscribe('work.code', log);
Arbiter.subscribe('work', log); // Subscribe to both `work.code` and `work`
Arbiter.subscribe('', log); // Subscribe to all messages
Arbiter.publish('work.code', {type: 'js', duration: 3600});

This library will only work in ES3 browsers if you provide an A+ compatible promise library. If you need one try ES6-promise.

Introduction

Promissory Arbiter is a pure JavaScript implementation of the publish-subscribe or Observer pattern. "Subscribers" subscribe to topics (or messages or channels) and the publishers send these messages or publications when they are ready for them. This allows components to be "loosely" coupled which can lead to more maintainable code, if used correctly.

Promissory Arbiter is asynchronous by default. This means that all subscribers are notified asynchronously of messages. This makes code easier to reason about especially when subscribers fire off additional events. This also lines up with the expectations of most other uses of the callback pattern in JavaScript effectively reducing cognitive load. Since you have been reading a while. Take a break by trying the following code in the console if you are having a hard time understanding what asynchronous means.

Arbiter.subscribe('my.topic', log);
Arbiter.publish('my.topic', data);
console.log('I execute _before_ the subscriber on the first line!');

If you are a synchronous kind of bird, you can set the synchronous option to true. If you try it in the console, remember that you changed it! For a more complete list of options checkout Options.

Arbiter.options.sync = true;

When publishing to the topic "a.b.c", all subscribers to "", "a", "a.b", and "a.b.c" receive the publication in priority order. If a subscriber in "a.b.c" has a higher priority than a subscriber to "", it will be notified before "". The "." separates the topic "generations" and every ancestor of a topic will be notified in addition to original publisher topic. "" is an ancestor of every topic (except itself). Try out the example on the top of the page or read more about topics in the Topics section below.

The last unique feature of promissory-arbiter is "promissory" features. When a publication is made, the publisher gets a promise that it can use to reason about the subscribers. The promise will resolve according to the specified options. By default, when all subscribers are complete the promise fulfills and if any fail, it rejects. This can be changed to allow for a number fulfilled or percent of all subscribers to be fulfilled. You can even relax the promise to be resolved when a number or percent resolves regardless of their success or failure status. In addition to this, it has settings for only running specified number of subscribers at at a time. All of this is documented in the Options section, or randomly guess the options until you have something like the following.

Arbiter.subscribe('get.data', getPeople, {priority: 10});
Arbiter.subscribe('get.data', getPlaces, {priority: 9});
Arbiter.subscribe('get.data', getThings, {priority: 8});
Arbiter.subscribe('get.data', getIdeas, {priority: 7});

// Let's pretend we are in IE7 and we can only have 2 ajax requests at a time.
Arbiter.publish('get.data', null, {semaphor: 2}).then(function(results) {
  // `results` is an array of the people, places, things, and ideas,
  // however only 2 of the subscriptions were ever pending at a time!
}, function(errs) {
  // If any of the publishers fail, you can have the errors here.
});

Topic

A Topic is simply a string. This string is can contain any character, but two characters have special meanings. The comma ',', with optional whitespace, separates individual Topics. This means that "a, b" is the same as two separate topics, ["a", "b"]. The second special character is the period or dot '.'. This character separates generations. In the example "a.b", "a" is an ancestor of it and "a.b.c" is a descendent of it. This creates a hierarchical relationship between topics. When you publish a topic all of the ancestors or parents are also notified. The empty string is an ancestor all topics. When publishing to "a.b.c.d", all subscribers to "", "a", "a.b", "a.b.c", "a.b.c.d" will be executed in priority order. If there is a subscriber to "" with a very high priority, it will be notified before "a" with a much lower priority.

Example

Arbiter.subscribe('', function f1 () {});
Arbiter.subscribe('a', function f2 () {});
Arbiter.subscribe('a.b', function f3 () {});
Arbiter.subscribe('a.c', function f4 () {});
Arbiter.subscribe('a.b.c', function f5 () {});
Arbiter.publish('a.b'); // Executes f1, f2, f3 in priority order

Subscription

Subscription(Object

This is either a Promise or a value used to communicate when the subscriber is done.

dataObject

The data associated with the publication.

, topicTopic

The topic to which the publication belongs.

, callbackfunction

A node style callback.

)

A Subscription is a function provided to the subscribe method. It is used as a callback when a publication occurs. The subscriber is considered "done working" if it returns a value (even undefined). If an error is thrown, then it is assumed that the subscriber failed. If it returns a Promise, then it is "done" when the Promise is fulfilled. If rejected, it is assumed to fail. If the subscriber function has a length of 3 or more, then it is provided with callback function as the third argument to be treated as a node-style callback. The first argument to the callback is the error and the second is the "return value".

Example

// All of the following look the same from a publishers perspective.
Arbiter.subscribe('my.topic', function() {
  return new Promise(function(fulfill, reject) {
    Math.random() > 0.5 ? fulfill('hi') : reject('bye');
  });
});

Arbiter.subscribe('my.topic', function(data, topic, done) {
  Math.random() > 0.5 ? done(null, 'hi') : done('bye');
});

Arbiter.subscribe('my.topic', function() {
  if (Math.random() > 0.5) {
    return 'hi';
  } else {
    throw 'bye';
  }
});

subscribe

subscribe(Token

A unique token to remove this subscription from the distribution list.

topicTopic | Array.<Topic>

The title of the topic to listen for publications. Topics are hierarchical can be separated by ",".

, subscriptionSubscription

The function to invoke every time a publication occurs. If subscription is not a function, a no-operation is put in its place.

, optionsObject

An object that can have two properties. ignorePersited and priority.

, contextObject

The value of this for the subscription.

)

Arbiter.subscribe registers a subscription to a topic and its descendants. When a publication occurs it will be notified. The behavior can be modified by using the options parameter. options.priority establishes the order to notify subscribers when multiple subscribers exist. The other option is ignorePersisted. This allows a subscriber to skip being notified of saved messages.

Example

Arbiter.publish('my.topic', null, {persist: true});
Arbiter.subscribe('my.topic', log, {ignorePersisted: true}); // => Nothing

publish

publish(PublicationPromise

This resolves according to Options

topicTopic

All subscribers to this topic, will be notified of the publication

, dataObject

This data is the publication that all subscribers will receive.

, optionsObject

These options override the options in Arbiter.options for this publication only. See Options for a complete list

)

Arbiter.publish notifies all subscribers of a publication by invoking their subscription function with the data and topic associated with the publication.

Example

var options = {persist: true, preventBubble: true};
Arbiter.publish('app.init', 'initialization', options);
Arbiter.subscribe('app', log); // => Nothing because of `preventBubble`
Arbiter.subscribe('app.init', log); // => logs app.init initialization

Options

persist
boolean

When true, subscribers are notified of past messages.

sync
boolean

When true, invokes the subscription functions synchronously.

preventBubble
boolean

When true, only the topics that match the published topics exactly are invoked.

latch
number

When this number is less than one, it is the ratio of subscribers that must fulfilled before resolving the PublicationPromise. If greater or equal to one, then it is a count of the subscribers that must fulfill.

settlementLatch
boolean

Changes the resolving logic of PublicationPromise to be based off resolved rather than fulfilled promises. This means that failed subscribers will count toward the tally of latch.

semaphor
number

The maximum number of subscribers to allowed to be pending at any given point in time.

updateAfterSettlement
boolean

If true, updates the PublicationPromise after it resolves.

Arbiter has a few options to affect the way that subscribers are notified and PublicationPromises are resolved.

Example

Arbiter.subscribe('a', log);
Arbiter.subscribe('a.b', log);
Arbiter.subscribe('a.b.c', log);
var promise = Arbiter.publish('a.b.c', {latch: 1});

// Remeber publish is async by default?
// promise.pending === 3;
// promise.fulfilled === 0;
// promise.rejected === 0;

PublicationPromise

fulfilled
number

The number of promises fulfilled when this promise settles.

rejected
number

The number of promises rejected when this promise settles.

pending
number

The number of promises pending when this promise settles.

token
Token

If the options.persist is true, then a token is added to the promise so it can be removed later.

publish returns a PublicationPromise. This is a regular old promise with some additional properties described below. Each property is updated in real time as updates occur; it stops updating when the promise fulfills. This can be changed with Arbiter.options.updateAfterSettlement.

Example

Arbiter.subscribe('get', getFromCache);
Arbiter.subscribe('get', getFromAjax);
Arbiter.publish('get', {latch: 1})
  .then(function(data) {
    // This is fulfilled when one of the subscribers fulfills because
    // of `latch: 1`. In this case we could also use `latch: 0.5`.
  }, function(errs) {
    // This occurs when it is impossible to satisify the latch. In this
    // case, both have to fail.
  });

unsubscribe

unsubscribe(Boolean

Returns false if the token's subscription cannot be located and true otherwise. This returns an array if multiple tokens or topics used.

tokenToken | Topic

Removes the subscription associated with the provided token. If a topic is provided, then this removes all subscribers and their descendants are removed.

, suspendBoolean

If this true, then the subscriptions are only suspended. This means that they will not be notified of any publications, but they can be re-enabled with [Arbiter.resubscribe].

)

Arbiter.unsubscribe removes the subscribers associated with a token or a topic. This prevents them from being notified when a publication occurs. By default these cannot be recovered, however this also allows us to temporarily suspend them instead.

Example

Arbiter.subscribe('a', function a () {});
Arbiter.subscribe('a.b', function ab () {});
var bToken = Arbiter.subscribe('b', function b () {});
Arbiter.subscribe('c', function c () {});
Arbiter.unsubscribe(bToken); // 'a', 'a.b', 'c' remain
Arbiter.unsubscribe('a'); // Only 'c' remains
Arbiter.unsubscribe(''); // Removes all subscriptions

resubscribe

resubscribe(Boolean

Returns false if the token's subscription cannot be located and true otherwise. This returns an array if multiple tokens or topics used.

tokenToken | Topic

The token or topic to reactivates

)

Reactivates all subscriptions associated with a token or all subscriptions that are descendants of a topic.

Example

Arbiter.subscribe('a, b, c', function() {}); // Create 3 listeners
Arbiter.unsubscribe('', true); // Suspends all listeners
Arbiter.resubscribe(''); // Resumes all listeners

removePersisted

removePersisted(Boolean

false if the topic or token does not exist, true otherwise.

tokenToken | Topic

The publication or topics to remove. Note: If provided the [PublicationPromise], then this uses the token associated with it.

)

Removes the publications that are stored (persisted) for late subscribers by providing either a Token or a Topic.

Example

Arbiter.publish('a', null, {persist: true});
Arbiter.subscribe('a', function a1 () {}); // Executes a1
Arbiter.removedPersisted();
Arbiter.subscribe('a', function a2 () {}); // Does not execute a2

create

Creates a new instance of Arbiter that is completely separate from the original. It has its own set of topics, subscribers, and options.

Example

var arbiter = Arbiter.create();
Arbiter.subscribe('a', function a () {});
arbiter.publish('a'); // Does not execute a