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(ObjectThis is either a Promise
or a value used to
communicate when the subscriber is done.
dataObjectThe data associated with the publication.
, topicTopicThe topic to which the publication belongs.
, callbackfunctionA 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(TokenA 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 ","
.
, subscriptionSubscriptionThe function to invoke every
time a publication occurs. If subscription
is not a function, a
no-operation is put in its place.
, optionsObjectAn object that can have two properties.
ignorePersited
and priority
.
, contextObjectThe 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(PublicationPromiseThis resolves according to
Options
topicTopicAll subscribers to this topic, will be notified of
the publication
, dataObjectThis data is the publication that all subscribers
will receive.
, optionsObjectThese 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
boolean
When true, subscribers are notified of past messages.
boolean
When true, invokes the subscription functions synchronously.
boolean
When true, only the topics that match the published topics exactly are invoked.
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.
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.
number
The maximum number of subscribers to allowed to be pending at any given point in time.
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
number
The number of promises fulfilled when this promise settles.
number
The number of promises rejected when this promise settles.
number
The number of promises pending when this promise settles.
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(BooleanReturns false if the token's subscription cannot be
located and true otherwise. This returns an array if multiple tokens
or topics used.
tokenToken | TopicRemoves the subscription associated with the
provided token. If a topic is provided, then this removes all
subscribers and their descendants are removed.
, suspendBooleanIf 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(BooleanReturns false if the token's subscription cannot be
located and true otherwise. This returns an array if multiple tokens
or topics used.
tokenToken | TopicThe 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(Booleanfalse
if the topic or token does not exist, true
otherwise.
tokenToken | TopicThe 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