subetha

0.0.2-alpha • Public • Published

SubEtha

PubSub over windows

by Bemi Faison

Description

SubEtha is a JavaScript library for channel-based communication between windows and domains. Use SubEtha to send and receive asynchronous events, implement ad hoc protocols, or define your own messaging framework.

Below demonstrates how you might communicate news updates using SubEtha.

var
  reporter = new Subetha.Client(),
  stories = ['Climate change reversed!', 'Hell froze over!'];

reporter.open('news@public')
  .on('::join', function (peer) {
    // make news inquiry
    peer.send('any news?');
  })
  .on('any news?', function (evt) {
    // respond to news inquiry
    evt.peer.send('latest', stories);
  })
  .on('latest', function (evt, headlines) {
    /* do something with headlines */
  });

How SubEtha Works

SubEtha takes publish-subscribe beyond the window, by utilizing localStorage as an event bus. The architecture consists of clients and bridges (provided as separate libraries/modules). Clients are instantiated in your code, and bridges are loaded in iframes. Clients communicate with bridges via postMessage, and bridges relay messages to each other via localStorage.

The other half of the transport mechanism are the protocols employed towards message integrity, security, and routing. Bridges perform an initial handshake, in order to establish a secure connection that includes message encoding. Bridges then authorize Clients to communicate with peers on a network -the origin of a bridge url and an arbitrary channel.

Usage

SubEtha is a "roll-up" of modules which add basic networking and messaging capability to your application.

As well, SubEtha works with the complimentary module SubEtha-Bridge.

Working with clients

To work with clients, first create a Subetha.Client instance. Clients provide a familiar event-emitter API for listening to both network and peer events.

var myClient = new Subetha.Client();

Subscribe to some network events. Network events inform you of changes in the connection state, and are prefixed with a double-colon ("::").

myClient
  .on('::connect', function () {
    /* set stuff up once we connect */
    this.emit('greeting', 'hello world!');
  })
  .on('::disconnect', function () {
    /* tear stuff down when we disconnect */
    this.emit('does', 'nothing', 'now');
  });

Subscribe to some peer events (these don't have a double-colon prefix). Peer events have arbitrary names, pass an event object, and may have any number of additional arguments.

myClient
  .on('foo', function (e) {
    console.log('Peer "%s" sent a message!', e.peer.id);
    e.peer.send('bar', 'plus', 'additional', 'args');
  })
  .on('bar', function (e) {
    console.log('Received "%s" from %s, on %s', e.type, e.peer.origin, e.sent);
  });

Now that your client is ready to "talk", connect to a channel and network using the #open() method. Pass it a string, formatted as _channel@url, where channel is an arbitrary identifier, and url is a url or alias to one (defined in Subetha.urls).

myClient.open('ecart'); // same as "ecart@local"

Note: The default url is the "local" alias.

Once a connection is established, you'll be able to access and communicate with your channel peers. Peers are represented as Subetha.Peer instances in the client.peers member. (Peer event subscribers can access the sending peer, as the event.peer member of the event-object.)

When done communicating, close the client connection. As a best practice, detach all event subscribers if you expect to discard your instance. (Invoke off() with no arguments.)

myClient.close();

if (allDone) {
  // detach all subscribers
  myClient.off();
}

See the Subetha Peer-Events module for more information.

Establishing an exchange

SubEtha comes bundled with the SubEtha Ad Hoc-Exchange module (or AX), for establishing stateful event logic. When you define an exchange, you're describing what events need to be passed back and forth with one peer, before executing logic. This could prove valuable in a peerless network, like SubEtha.

Below demonstrates using AX to vet peers before responding to an event.

var trusty = new Subetha.Client().open('wild-west@public');

// set up two round exchange to flag peers
trusty
  .adhoc('yo yo!', function (convo) {
    convo.reply('who is it?!');
  })
  .adhoc('yo yo!', 'who is it?!', 'me fool!', function (convo, someAuthToken) {
    if (isTokenValid(someAuthToken)) {
      // let the peer know they are now trusted
      convo.reply('wassup!?');
      // capture result of exchange in peer
      convo.peer.trusted = true;
    } else {
      convo.end();
    }
  });

// only respond to "trusted" peers
trusty.on('gimme data', function (evt) {
  if (evt.peer.trusted) {
    evt.peer.send('data', getPriviledgedData());
  }
});

See the Subetha Ad Hoc-Exchange module for more information.

Working with Bridges

Bridges provide access to a network - specifically the url origin of the bridges iframe container - and require separately hosted files. Two bridge urls are provided by default, so you can get up and running. They're aliases, defined in Subetha.urls (feel free to add your own alias). All bridges use the Subetha-Bridge module.

The first bridge-alias is "local", a JavaScript url that uses the same domain as your application. The url loads a public copy of the bridge module. If you have a locally hosted copy, you can override the url with your own JavaScript url.

The second bridge-alias is "public", a publicly hosted, open-source web page that accepts any client and any channel. Eventually this bridge will captures non-critical usage information, so everyone can get a sense of SubEtha's usage.

See the Subetha-Bridge module for more information.

Authenticating clients

Though bridges receive lots of information about connecting clients, some may require credentials. The value of the Client.credentials member is passed to a bridge, when opening a connection; it's converted/wrapped as an array for submission. You may also pass credentials via the Client#open() method. Failed authorizations result in a ::auth-fail event.

var client = new Subetha.Client();
client.open('club@bouncer', 18, 'please!');

console.log('credentials sent:', client.credentials); // => credentials sent: [18, "please!"]

client.on('::auth-fail', function () {
  console.log('no one under 21 allowed!');
});

Note: The example above presumes that the alias "bouncer" has been defined.

Extending the client module

While SubEtha routes messages, and communicates with bridges privately (via a closure), the module does offer some customization. Below are the classes available on the SubEtha namespace.

  • Subetha.Client - The primary network agent, used to send and receive messages.
  • Subetha.Peer - A proxy to other clients, for referencing and communication.
  • Subetha.EventEmitter - A canonical event-emitter class, inherited by Subetha.Client.

Sending & receiving custom messages

The Subetha-Client module lets you send custom message types, to encapsulate and enforce logic between known peers. For example, the bundled AX plugin defines a "subetha/exchange" message type, and can therefore converse with peers using the same module. Similarly, the bundled PE plugin, defines a "subetha/event" message type.

Below demonstrates defining a "boot" message type. Methods for sending this message type have been added to the Client and Peer prototypes. As well, a boot message handler has been added to the Subetha.msgType namespace.

// add Client method for sending this custom type
Subetha.Client.prototype.kickAll = function () {
  // broadcast messages by omitting a peer id
  this._transmit('boot');
};

// add Peer method for sending this custom type
Subetha.Peer.prototype.kick = function () {
  // send message to this peer only
  this._client._transmit('boot', this);
};

// add handler for when this message targets a client
Subetha.msgType.boot = function (receivingClient) {
  // simply close the receiver of this message
  receivingClient.close();
};

The Client#_transmit() method sends any message type you like. (See the PE module for more information on this method.) Keep in mind that for any custom message to work, a corresponding message handler must exist on the recieving peer.

Overriding the EventEmitter

You may override the Subetha.EventEmitter class, to incorporate a more familiar or powerful API than what SubEtha provides. The EventEmitter class is inherited by the Subetha.Client class.

The code below demonstrates how to supplement SubEtha's EventEmitter with BackBone.Events. BackBone events allow you to do interesting things, like listen to all incoming events, or unsubscribe after the event fires.

_.extend(Subetha.EventEmitter.prototype, Backbone.Event);
// map `fire` to `trigger`, since SubEtha uses that method internally
Subetha.EventEmitter.prototype.fire = Backbone.Event.trigger;

Note: This example presumes you have BackBone and Underscore in your environment.

API

API documentation is available in the repositories of the respective modules that comprise the SubEtha module.

Installation

SubEtha works within, and is intended for, modern JavaScript browsers. It is available on bower, component and npm as a CommonJS or AMD module.

If a SubEtha isn't compatible with your favorite runtime, please file an issue or pull-request (preferred).

Dependencies

SubEtha depends on the following modules:

Web Browsers

Use a <SCRIPT> tag to load the subetha.min.js file in your web page. The file includes SubEtha dependencies for your convenience. Doing so, adds Subetha to the global scope.

  <script type="text/javascript" src="path/to/subetha.min.js"></script>
  <script type="text/javascript">
    // ... SubEtha dependent code ...
  </script>

Note: The minified file was compressed by Closure Compiler.

Package Managers

  • npm install subetha
  • component install bemson/subetha
  • bower install subetha

Caution: The npm dependencies load the Subetha-Client modules as a peerDependency.

AMD

Assuming you have a require.js compatible loader, configure an alias for the SubEtha module (the term "subetha" is recommended, for consistency). The subetha module exports a module namespace.

require.config({
  paths: {
    subetha: 'my/libs/subetha'
  }
});

Then require and use the module in your application code:

require(['subetha'], function (Subetha) {
  // ... SubEtha dependent code ...
});

Warning: Do not load the minified file via AMD, since it includes SubEtha dependencies, which themselves export modules. Use AMD optimizers like r.js, in order to roll-up your dependency tree.

Considerations

SubEtha is an experimental project in alpha development. Testing is non-existent. Production deploys are discouraged, and done at your own risk.

The following sub-sections express areas that are under active or planned development and/or improvement.

Security

As a new "pipe", SubEtha intends to be as robust and secure as TCP/IP and SSL connections. However, since this is only an ideal: security is as security does. Do not send with SubEtha, that which you do not want to share.

Capacity

Despite localStorage allotting 5Mb per domain, SubEtha does not check or assess message size. Do not send base64 encoded files... yet.

Encoding/Decoding

SubEtha currently encodes and decodes outgoing messages (from bridge to the client) synchronously. Large messages will likely cause noticeable lag.

Shout outs

License

SubEtha is available under the terms of the Apache-License.

Copyright 2014, Bemi Faison

Package Sidebar

Install

npm i subetha

Weekly Downloads

3

Version

0.0.2-alpha

License

none

Last publish

Collaborators

  • bemson