Arbiter.js - my Pub/Sub implementation API. Any thoughts?

M

Matt Kruse

I've written my own "pub/sub" module. Below is the API. Any thoughts?

I wrote my own because I didn't find one that I liked, and that had
all the features I wanted/needed. Some of the features in mine are:
- Subscribe to multiple events with a single function
- Subscribe to wildcards, like all "game/*" messages
- Set priority on subscribers so they fire in order
- Allow subscribers to pass data between them for the life of the
event
- Asynchronous or synchronous publishing of messages
- persistent messages that fire for new subscribers AFTER the publish
has happened
- Subscribers may cancel the bubble
- library agnostic

The code is actually fairly simple and has been working well already.
I plan to publish it soon for others to use. Here is the API. See if
it makes sense?

/*
Arbiter.js
by Matt Kruse

Arbiter.js is an implementation of the Pub/Sub (Observer) pattern. It
allows
messages to be published to a central controller (Arbiter), which then
pushes
the messages to all subscribers.

=======================================================================
PUBLISH a message

Arbiter.publish( msg [, data [, options] ] )

Returns: true on success, false if any subscriber has thrown a js
exception
=======================================================================

// Simple publish
Arbiter.publish( 'component/msg' );

// Publish with data, which will be passed to subscribers
Arbiter.publish( 'component/msg' , {"data":"value"} );

// Don't allow subscribers to cancel the message bubble
Arbiter.publish( 'component/msg' , null, {cancelable:false} );

// Make a message persistent. This message will be delivered to any
code that
// subscribes even after the publish has finished
Arbiter.publish( 'component/msg' , null, {persist:true} );

// Fire the subscribers asynchronously, so their execution doesn't
// delay the rest of your code from running
// (Note: async messages cannot be cancelled by subscribers)
Arbiter.publish( 'component/msg', null, {async:true} );

=======================================================================
SUBSCRIBE to a message

Arbiter.subscribe( msg, func )
Arbiter.subscribe( msg, options, func )
Arbiter.subscribe( msg, options, context, func )
Returns: subscription id
or [id1,id2] if subscribing to multiple messages

Subscribed Function = function( published_data, msg, subscriber_data )
{}
- "this" will be set to context, if passed in
- published_data = data published along with the message, if any
- msg = the message published
- subscriber_data = data that subscribers can set arbitrarily, which
will
be passed to subsequent subscribers
=======================================================================

// Simple subscribe
Arbiter.subscribe( 'component/msg', function() { } );

// Subscribe to multiple messages at once with the same function
// Note: The 2nd argument to the subscriber is the message string!
Arbiter.subscribe( 'component/msg, component/msg2', function() { } );

// Subscribe to all messages matching a wildcard expression
Arbiter.subscribe( 'component/*', function() { } );

// Subscribe to ALL messages (useful for logging, for example)
Arbiter.subscribe( '*', function() { } );

// Set priority so subscribers get called in proper order
// (default priority=0, positive=higher priority)
Arbiter.subscribe( 'msg', {priority:10}, func(){} );
Arbiter.subscribe( 'msg', {priority:20}, func(){} ); // Called first!

// If there are persisted messages, call for those too
// (Both publisher and subscriber must request this for it to work)
Arbiter.subscribe( 'msg', {persist:true}, func(){} );

// Set the subscriber context (this)
Arbiter.subscribe( 'msg', null, document.getElementById('x'),
function() {
x.innerHTML = "Message handled!";
}
);

=======================================================================
UNSUBSCRIBE / RESUBSCRIBE to a message

Arbiter.unsubscribe( subscription_id )
Returns: true if unsubscribe successful, else false

Arbiter.resubscribe( subscription_id )
Returns: true if resubscribe successful, else false
=======================================================================

var subscription_id = Arbiter.subscribe( 'msg', function(){} );
Arbiter.unsubscribe( subscription_id );
Arbiter.resubscribe( subscription_id );

*/
 
M

Matt Kruse

Looking good so far, very clear and intuitive API.
Thanks

You wrote that publish() returns false if one of the subscribers throws
an exception. Is this the intended way for subscribers to cancel the
event, or is there another reason why you're catching/suppressing these
exceptions?

I suppress the errors because if one badly-written subscriber throws
an exception, it would kill all the rest. I don't want to prevent
subscribers from getting the message just because one subscriber
failed.

Subscribers can return false to cancel the event. The publish method
only returns false if there was an exception from one of the
subscribers, so the publisher can know if something needs to be done.
Any idea of a better way to handle subscriber exceptions?
Do you have to explicitly create an Arbiter object (with 'new' or a
factory method) before using it? Is it a singleton, or can there be more
than one active Arbiter at the same time?

No, Arbiter is in the global namespace. Right now, there can't be more
than one Arbiter, but I realize there may be a need to create separate
controllers for specific areas. I haven't thought much about that yet.
Is the format "component/message" fixed? I assume it is, because it
looks like there are at least three special characters that can't be
used in message names: space, comma, and asterisk.

It's not fixed, but you're right, messages cannot include those
characters. Many pub/sub implementations actually use messages like /
this/that/etc by convention. But it could just as easily be "a-b-c" or
even "a_b_c" or just "abc". Using "category/message" makes it easy to
subscribe to entire categories of events, using "category/*".

Matt Kruse
 
S

Scott Sauyet

I've written my own "pub/sub" module. Below is the API. Any thoughts?

A number of them. But first off, I want to say that I like it quite a
lot.

I wrote my own because I didn't find one that I liked, and that had
all the features I wanted/needed. Some of the features in mine are:
 - Subscribe to multiple events with a single function
 - Subscribe to wildcards, like all "game/*" messages
 - Set priority on subscribers so they fire in order
 - Allow subscribers to pass data between them for the life of the
event
 - Asynchronous or synchronous publishing of messages
 - persistent messages that fire for new subscribers AFTER the publish
has happened
 - Subscribers may cancel the bubble
 - library agnostic

I've been starting to design my own for my current project, with a
very similar list of requirements. There were only two differences.
I hadn't seen any need for the asynchonous publishing (although I like
it.) And I also want to be able to do pattern matching for
subscribers on the data parameter. This puts mine somewhere between
standard pub/sub and tuple spaces. Had you considered anything like
this? In yout API, it might look like:

Arbiter.subscribe('component/msg',
{match: {type: 'x'}},
function() {
alert('Function run');
}
);

// no alert
Arbitrer.publish('component/msg', {type: 'y', value: 'abc'});

// alert
Arbitrer.publish('component/msg', {type: 'x', value: 'def'});

I don't know if you would find value in that, but I find it pretty
powerful. My matching code is able to test all types of values,
nested structures, or use regexes or arbitrary functions to check for
matches. It is still pretty new, and might not be performant, but it
works well for me so far. (I can post it somewhere if you're
interested.)


Arbiter.publish( 'component/msg' );
Arbiter.publish( 'component/msg' , {"data":"value"} );
Arbiter.publish( 'component/msg' , null, {cancelable:false} );

I generally prefer APIs that don't require you to pass a null
parameter in the middle of a list. Have you considered this
alternative?:

Arbiter.publish('msg', {
data: {prop1: 'val1', prop2: 'val2'},
options: {persist: 'true', cancelable: 'false'
});

In this both `data` and `options` are optional, as is the entire
second parameter. I'm not convinced that this is any better, but it's
at least something to consider.

Arbiter.subscribe( 'component/msg, component/msg2', function() { } );

This is arguably cleaner, and probably easier to implement and to
test:

Arbiter.subscribe( ['component/msg', 'component/msg2'], function()
{ } );

You would need check only if the parameter is a sting or is array-
like, with no string manipulation. And you would no longer make
commas or spaces special characters.


// If there are persisted messages, call for those too
// (Both publisher and subscriber must request this for it to work)
Arbiter.subscribe( 'msg', {persist:true}, func(){} );

Why should the subscriber care whether the message was persisted? I
understand that the publisher might want to keep the message around
for any newly-established subscribers, for things such as 'app/
initialized'. But when would a subscriber care that the message is
not fresh?

// Set the subscriber context (this)
Arbiter.subscribe( 'msg', null, document.getElementById('x'),
                   function() {
                      x.innerHTML = "Message handled!";
                   }
                 );

do you mean `this.innerHTML = "Message handled!";` ?

I have a few questions about the asynchronous mechanism. Are all
subscribers run synchronously inside a single delayed call? If so,
why can't it be cancelled? If not, why not; and how then does it
interact with the priority ordering? (Which also reminds me that the
to me using larger numbers to indicate that it's supposed to run
earlier seems counterintuitive; but I know that has no ideal
solution.) Also, should publishers be allowed to declare that they
are to be run asynchonously? Obviously they could do that internally
in their functions, but wouldn't it be nice to do it in the Arbiter?

To reiterate, I do like this very much. I'm amazed just how close our
requirements are.

I look forward to seeing the implementation.

-- Scott
 
M

Matt Kruse

A number of them.  But first off, I want to say that I like it quite a
lot.

Cool, thanks for the feedback
it.)  And I also want to be able to do pattern matching for
subscribers on the data parameter.  This puts mine somewhere between
    Arbiter.subscribe('component/msg',
      {match: {type: 'x'}},
      function() {
        alert('Function run');
      }
    );

I'm curious why you would not just want to inspect the data in the
subscriber function itself?
I generally prefer APIs that don't require you to pass a null
parameter in the middle of a list.  Have you considered this
alternative?:

I generally don't either, but I also dislike API's that require
everything to be wrapped up in objects with named parameters. The code
just gets a little more confusing. In theory, we could ALWAYS just
pass a single object as an argument and use it as named parameters,
right? In some cases, that works well. But in my case, I thought that
the optional parameters would be rarely used, so it would be more
convenient to allow simple parameter passing. But, your point is
surely taken.
Arbiter.subscribe( 'component/msg, component/msg2', function() { } );
This is arguably cleaner, and probably easier to implement and to
test:
    Arbiter.subscribe( ['component/msg', 'component/msg2'], function()
{ } );

That's a good idea. I will actually code for both cases. For my
internal purposes, passing a delimited string is very convenient for
some of my implementations.
Why should the subscriber care whether the message was persisted?

My thought was that if a publisher decides to persist something, there
may be 50 old messages. If I subscribe, I may only want to consider
the new ones added, but I'll get all the old ones also. Since
persisted messages are kind of a special case, I thought it should
take an agreement from both sides to process them.

But I also see your point - publishers should really only publish
persisted messages if they really need to be persisted, right? And the
subscriber shouldn't have to care either way?
do you mean `this.innerHTML = "Message handled!";` ?

Yes, thanks for pointing it out ;)
I have a few questions about the asynchronous mechanism.  Are all
subscribers run synchronously inside a single delayed call?

Nope, I am calling setTimeout() on each subscrber, with a delay of
10ms for the first, 11 for the second, 12, etc. That's why it can't be
canceled. It's really de-coupling all the subscribers.
 (Which also reminds me that the
to me using larger numbers to indicate that it's supposed to run
earlier seems counterintuitive; but I know that has no ideal
solution.)

I used zIndex as an example - the higher the number, the higher in the
stack. I wonder if others would find it more intuitive to use higher
or lower first?
 Also, should publishers be allowed to declare that they
are to be run asynchonously?  Obviously they could do that internally
in their functions, but wouldn't it be nice to do it in the Arbiter?

You mean subscribers? If so, then yes, that's a good idea too.
I look forward to seeing the implementation.

Will post soon. I have it running, just not in a form that I want to
publish to the world yet ;)

Thanks!

Matt
 
M

Matt Kruse

I hadn't seen any need for the asynchonous publishing (although I like
it.)

In fact, I'm now wondering if async should be the default. What do you
think?

If any subscribers change the UI, the browser may not do a repaint
until the whole message cycle ends, which is a bummer. If async was
default, then each subscriber would at least be able to render its
changes before the next subscriber runs.

I'm imagining the code that would be needed to allow async subscribers
to cancel the bubble also. It's a bit more complex, but not
impossible.

Matt
 
S

Scott Sauyet

Matt said:
Cool, thanks for the feedback
I'm curious why you would not just want to inspect the data in the
subscriber function itself?

For the same reason that you wouldn't want to send all messages to all
subscribers, and let them sort it out by checking the topic string
themselves. I would like my handlers to be as simple as possible, and
push off to the PubSub system the checking of appropriate matching.
I'm finding this a reasonably happy middle ground between a Blackboard
or Tuple-Space system where all listeners have a chance to check all
messages and a naive pub/sub system where the only method of routing
messages is a simple string topic. Mine has additional flexibility,
at a cost of additional implementation complexity. The user API is
still reasonably clean.

Arbiter.subscribe( 'component/msg, component/msg2', function() { } );
This is arguably cleaner, and probably easier to implement and to
test:
    Arbiter.subscribe( ['component/msg', 'component/msg2'], function()
{ } );

That's a good idea. I will actually code for both cases. For my
internal purposes, passing a delimited string is very convenient for
some of my implementations.

Okay, but be careful of bloating things by allowing both array and
delimited string options. I personally would choose only one. If the
delimited one meets your needs better, by all means use it. I would
have no problem with accepting either 'single/string' or ['array',
'of', 'strings'] as the latter is a clear-cut generalization of the
former. But offering both ['array', 'of', 'strings'] and 'delimited,
list, of, strings' to me just confuses the issue.

My thought was that if a publisher decides to persist something, there
may be 50 old messages. If I subscribe, I may only want to consider
the new ones added, but I'll get all the old ones also. Since
persisted messages are kind of a special case, I thought it should
take an agreement from both sides to process them.

But I also see your point - publishers should really only publish
persisted messages if they really need to be persisted, right? And the
subscriber shouldn't have to care either way?

Yes, and it's a little awkward either way.

Nope, I am calling setTimeout() on each subscrber, with a delay of
10ms for the first, 11 for the second, 12, etc. That's why it can't be
canceled. It's really de-coupling all the subscribers.

What's the advantage of this over simply delaying the execution of the
entire block of subscribers?

You mean subscribers? If so, then yes, that's a good idea too.

Yes, subscribers. "When I hear my topic announced, I'm going to go do
some complicated Ajax magic. Don't wait up." :)

Will post soon. I have it running, just not in a form that I want to
publish to the world yet ;)

Sounds like most of my code!

Cheers,

-- Scott
 
M

Matt Kruse

For the same reason that you wouldn't want to send all messages to all
subscribers, and let them sort it out by checking the topic string
themselves.

Point taken. I think I will save this on my list of ideas for version
2.
Okay, but be careful of bloating things by allowing both array and
delimited string options.

Internally, I just .split() the string into an array anyway, so I
wrapped that with
if (typeof messages=="string")
and now it accepts both.

I don't mind limiting messages and saying they cannot have spaces or
commas. That seems reasonable. And this adds a little flexibility.

In my code, I actually write this often, when an array is needed:

"a,b,c,d".split(/,/)

For multiple values, I find it clearer, easier and less error-prone
than matching up lots of quotes and commas.
Yes, and it's a little awkward either way.

Agreed. Here's my compromise - subscribers will by default get all
persisted messages. They can supply an option to not get them. Seems
fair.
What's the advantage of this over simply delaying the execution of the
entire block of subscribers?

Well, if one breaks, it won't prevent the next from running. Also, the
first function won't be prevented from updating the UI by a long-
running subsequent subscriber. Its UI changes will be reflected in the
browser as soon as it exits.
Yes, subscribers.  "When I hear my topic announced, I'm going to go do
some complicated Ajax magic.  Don't wait up."  :)

Just implemented it. Thanks :)
Sounds like most of my code!

I hear ya! Code never seems finished enough to post for public use.
I'm reminded of the slogan they have on the walls at Facebook - "Done
is better than Perfect".
I'm not sure I agree with the tone of that, but in some respects I
like it :)

Matt
 
M

Matt Kruse

Well, I've thrown it up on the web in a pre-alpha form:
http://ArbiterJS.com

I need to do more with it, but in the interest of "Done is better than
Perfect", feel free to take a look :)

Matt
 
S

Scott Sauyet

Well, I've thrown it up on the web in a pre-alpha form:http://ArbiterJS.com

I need to do more with it, but in the interest of "Done is better than
Perfect", feel free to take a look :)

Matt

I don't have time to look very closely today, but it looks good at a
quick overview. I will try to spend more time looking at it soon.

-- Scott
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,763
Messages
2,569,562
Members
45,039
Latest member
CasimiraVa

Latest Threads

Top