subetha-client
v0.0.2-alpha
Published
Define, send and receive SubEtha message types
Downloads
11
Maintainers
Readme
SubEtha-Client
Define, send and receive SubEtha message types
by Bemi Faison
Description
SubEtha-Client (or Client) is a supporting library within the SubEtha messaging architecture. A client sends and receives messages with network peers - other clients, connected to the same channel and url origin.
Clients primarily implement the SubEtha protocol for communicating with Bridges, and provide a framework for communicating with each other.
Note: Please see the SubEtha project page, for important background information, plus development, implementation, and security considerations.
Usage
The Client module runs in a web page that is shared with your application code. To join a network, Clients work with the SubEtha-Bridge module, via iframes. To exchange messages, Clients send and receive pre-defined message types.
See the Peer-Events and Ad Hoc-Exchange modules, for examples that use this module for custom messaging.
Networking with SubEtha
The Client module manages the process of joining a SubEtha network. One part involves loading a Bridge module within an iframe. The other authorizes a Client on the network. (Since the first part risks network error, it is limited to 10 seconds - a threshold you may change via the Subetha.bridgeTimeout
member.)
Client instances start this connection sequence, by invoking the Client#open()
method.
var client = new Subetha.Client();
client.open('some [email protected]/bridge.html');
The method accepts a network identifier as it's first argument; a string, formatted as "channel@url", where channel is an arbitrary value, and url is a web page hosting the Bridge module. For convenience, "@url" may be an alias, referencing a key from Subetha.urls
.
Connecting to common bridges
The Client module supplies two bridges by default: the "local" and "public" aliases.
The "local" alias points to a JavaScript-URL that loads the Bridge module from a github CDN. As a dynamic iframe, the bridge utilizes the same domain as the client. Use this when you only want to communicate with windows to your own application/domain.
The "public" alias points to a webpage on a github CDN. This bridge utilizes the CDN's domain. Use when you want to communicate with applications in different domains.
By default - i.e., if Client#open()
were called with no arguments - Clients will connect to "lobby@local", or the "lobby" channel of the "local" bridge url alias.
Monitoring the connection status
Once connected, Clients fire a ::connect network event. Similarly, you can subscribe to the ::error and/or ::auth-fail events. Any of these events inform when and how a connection attempt completes.
client.on('::connect', function () {
console.log('Woot!');
});
client.on('::error', function () {
console.log('Something went wrong...');
});
client.on('::auth-fail', function () {
console.log('I hate rejection.');
});
Likewise, Clients fires a ::disconnect network event, once disconnected. This event only occurs after having successfully connected to a network - i.e., if the ::connect event fired.
client.on('::disconnect', function () {
console.log('No longer connected');
});
Tracking channel peers
Connected clients receive a list of existing peers (in their channel), via the peers
property. Each peer is a Subetha.Peer
instance, linked to the client via the Peer@_client
property. (The underscore implies private by convention; the property should not be used directly.)
var person = new Subetha.Client().open('party@public');
person.on('::connect', function () {
var
me = this,
peerId,
count = 0;
for (peerId in me.peers) {
if (peers.hasOwnProperty(peerId)) {
count++;
}
}
console.log('Partygoers so far: %d', count);
});
Clients can observe newly added peers by subscribing to the ::join event. Likewise departing peers can be observed via the ::drop event. Both events pass the effected Peer instance.
person.on('::join'. function (peer) {
console.log('Welcome peer %s', peer.id);
});
person.on('::drop', function (peer) {
console.log('Sorry to see you go %s', peer.id);
});
Note: A ::join event is also fired per existing peer, once a client connects. These events pass an additional truthy flag, indicating that the peer was present before the client connected.
Connecting to protected bridges
Some bridges may be coded to require some form of access verification. You can use the Client#open()
method or the Client@credentials
property to pass authorization credentials. Bridges that deny authorization cause the client to fire an ::auth-fail network event.
Below demonstrates two clients that use each approach to open a connection.
var
manual = new Subetha.client(),
automatic = new Subetha.client();
manual.channel = 'do it';
manual.url = 'forme';
manual.credentials = ['user', 'access-token'];
// uses the properties above
manual.open();
// sets the properties above
automatic.open('do it@forme', 'user', 'access-token');
console.log('channel: "%s"', automatic.channel); // channel: "do it"
console.log('url: "%s"', automatic.url); // url: "forme"
console.log('credentials: "%s"', automatic.credentials.join('", "')); // credentials: "user", "access-token"
Note: This example assumes the "forme" bridge alias was already defined.
Messaging with SubEtha
SubEtha allows you to send arbitrary message types, and requires defining the message types you want to receive. In general, Clients should be able to receive the message types they send. Thus, it's recommended that both implementations be grouped in the same code base, as a module or plugin. This allows you to share your messaging logic and communicate with other applications and/or windows.
Below demonstrates code that enables both sending and receiving the message type "hug".
// add Client method to broadcasts hugs
Subetha.Client.prototype.hugAll = function (kind) {
this._transmit(
'hug', // type
null, // target all peers (when falsy)
kind || 'normal' // payload
);
};
// add Peer method to send one hug
Subetha.Peer.prototype.hugOne = function () {
this._client._transmit(
'hug', // type
this, // target this peer
'one on one' // payload
);
};
// add a hug message handler
Subetha.msgType.hug = function (toClient, fromPeer, payload) {
console.log(
'%s received a %s hug from %s!',
toClient.id,
payload,
fromPeer.id
);
};
In practice, the details of the "hug" message type are hidden by these prototyped methods and the static message-type handler. Below creates a greeter
client that eventually sends a "hug" message to the newb
client (depending on whom joins the network first).
var
greeter = new Subetha.Client().open('support group'),
newb = new Subetha.Client().open('support group');
greeter.on('::connect', function () {
greeter.hugAll('big');
});
greeter.on('::join', function (peer) {
peer.hugOne();
});
Note: These code snippets work when SubEtha is loaded via a SCRIPT tag. Additional wrappers would let them work in AMD/CJS environments.
Sending messages
SubEtha allows connected clients to send arbitrary messages to peers (i.e., clients in the same channel and url origin). All messages have a type, payload, and recipient(s). The payload is converted to JSON automatically, but must adhere to the structured clone algorithm, or unexpected errors may occur.
Messages are sent via the Client#_transmit()
method, which is intended for use via methods you prototype on the Subetha.Client
and/or Subetha.Peer
classes. This approach allows you to hide implementation details, such as the payload structure and/or message validation.
Subetha.Client.prototype.canSend = false;
// only transmits when local logic permits
Subetha.Client.prototype.sendSomething = function () {
if (this.canSend) {
this._transmit('some_message');
}
};
The Client module only checks to ensure both the client and recipient(s) are active peers. (You can avoid such checks by broadcasting your message - i.e., pass a falsy value as the second parameter.) If the message is cleared for transmission, then the #_transmit()
method returns true
. Otherwise, false
.
Note: A successful transmission does not imply that the message was received or processed by it's recipient(s).
Receiving messages
The Client module only routes messages that have an active recipient and sender, plus a handler function for the given message type (or, "message handler"). Message handlers must be set in the Subetha.msgType
collection, keyed under their target message type.
For example, the code below adds a message handler for the message type "subetha/foo".
Subetha.msgType['subetha/foo'] = function () {
console.log('a client received a "subetha/foo" message!');
};
In this way, message types are a kind of namespace which you must choose carefully. In a mixed environment your handler may be overridden, or receive messages with an unexpected structure. Verbosity is recommended, since the message type will often be hidden to your application logic.
The Client module invokes message handlers with the following signature.
- toClient - The Client instance receiving the message.
- fromPeer - The Peer instance that sent the message. This instance comes from the receiving Client's own
peers
collection. - payload - The arbitrary value sent from the peer, via the
#transmit()
method. - details - Message meta-data, such as routing and metrics.
*
id
- The message id. *sent
- The date the message was sent (as aDate
instance). *timeStamp
- The date the message was received (in milliseconds). *peers
- An array of all recipient ids. (This is empty for broadcasts.)
Below demonstrates a message handler that validates the message content and meta-data, before triggering events in the client.
Subetha.msgType.paint = function (toClient, fromPeer, payload, details) {
var oldHue = toClient.hue;
if (
// ensure the payload is a hex-RGB string
/^[a-f0-9]{6}$/i.test(payload) &&
// ensure this client was the only targeted recipient
details.peers.length == 1 &&
// the message didn't take too long to arrive
details.timeStamp - details.sent < 100 &&
// the sender was from a specific origin
/\bexample.com\b/.test(fromPeer.origin)
) {
toClient.hue = payload;
toClient.fire('hueChange', payload, oldHue);
}
};
API
Below is reference documentation for the SubEtha-Client module. This module provides the Subetha
namespace, which features several classes, exposed for extension and customization.
Note: Instance methods are prefixed with a pound-symbol (#
). Instance properties are prefixed with an at-symbol (@
). Static members are prefixed with a double-colon (::
).
Subetha::Client
Creates a new Client instance.
var client = new Subetha.Client();
This class inherits from Subetha.EventEmitter
.
Network events
Client instances fire the following network events, prefixed by a double-colon (::
).
- ::auth-fail - Triggered when a connection attempt is denied.
- ::connect - Triggered when a connection is established.
- ::disconnect - Triggered when the client connection ends.
- ::drop - Triggered when a peer leaves the channel.
peer
: (Peer) References the recently removedSubetha.Peer
instance.
- ::join - Triggered when a peer joins the channel.
peer
: (Peer) References the newly addedSubetha.Peer
instance.exists
(Boolean) This istrue
for peers present during the "::connect" event.
- ::readyStateChange - Triggered when
@state
changes. This event precedes ::auth-fail, ::connect and ::disconnect events.newState
: (number) The integer of the new state.oldState
: (number) The integer of the old state.
Client#close()
Close the client connection.
client.close();
Client#open()
Establish a connection to a specific channel and (bridge) url.
client.open([network [, credentials, ... ]);
- network: (string) The channel and bridge to authorize, in the format "channel@url". The parsed value updates the
@channel
and/or@url
properties. - credentials: (mix) Any additional arguments replace
@credentials
and are sent to the bridge.
Opening closes the existing connection, unless it is the same channel and bridge url.
Client#_transmit()
Send a message to one or more peers.
client._transmit(type [, peers [, payload]]);
- type: (string) The message type.
- peers: (string[]|peer[]) One or an array of recipient peer ids and/or instances. When omitted or falsy, the message is broadcast to all channel peers.
- payload: (mix) An arbitrary value passed to a message type handler.
Note: Peers must have a message handler that matches the type sent, in order for them to both receive and process your message.
Returns true
when the message is successfully sent. Otherwise false
.
Client@channel
A string reflecting the network channel. The default value is "lobby".
Client@credentials
A value sent when establishing a client connection.
The value is converted to an array before transmission. A value of null
or undefined
is ignored.
Client@id
A unique hash to identify this instance in a network. This property is updated during each connection attempt.
Client@state
A number reflecting the current connection status. There are five possible states:
0
- The initial state, when there is no connection.1
- The queued state, when a connection request is queued.2
- The pending state, when a connection request has been sent.3
- The ready state, when a connection has been established.4
- The closing state, when the connection is closing.
Note: The ::readyStateChange event fires after this value changes.
Client@url
A string reflecting the network url or alias. The default value is "local".
Subetha::EventEmitter
Creates a new EventEmitter instance.
var eventer = new Subetha.EventEmitter();
This class is not meant for direct instantiation.
EventEmittter#fire()
Triggers callbacks, subscribed to this event.
eventer.fire(event [, args, ... ]);
- event: (string) The event to trigger.
- args: (mix) Remaining arguments that should be passed to all attached callbacks.
EventEmittter#on()
Subscribe a callback to an event.
eventer.on(event, callback);
- event: (string) An arbitrary event name.
- callback: (function) A callback to invoke when the event is fires.
EventEmittter#off()
Unsubscribe callback(s) from an event. When invoked with no arguments, all subscriptions are removed.
eventer.off([event [, callback]]);
- event: (string) The event to unsubscribe. When omitted, all event subscribers are removed.
- callback: (function) The callback to detach from this event. When omitted, all callbacks are detached from this event.
Subetha::Peer
Creates a new Peer instance.
var peer = Subetha.Peer(cfg, client);
- cfg: (object) Configuration values for peer properties.
- client: (Client) The client instance that will reference this peer.
This class is not meant for direct instantiation.
Peer@channel
A string reflecting the network channel.
Peer@id
A hash to uniquely identify this peer in a network.
Peer@origin
The url origin of the web page hosting the peer.
Peer@start
A Date
instance indicating when the peer joined the network.
Subetha::guid()
Returns a unique hash of characters.
var hash = Subetha.guid();
Subetha::bridgeTimeout
The number of milliseconds to wait before aborting a connection attempt with a Bridge module. The default value is 10000
or 10 seconds.
Subetha::msgType
Hash of message handling functions, keyed by the message type they handle. (For instance, a built-in type is "event", for handling event messages.) This property is meant for library authors, extending the SubEtha module.
Below is the call signature passed to message handlers.
- toClient - The Client instance receiving the message.
- fromPeer - The Peer instance that sent the message.
- payload - A custom, arbitrary value sent from the peer.
- details - Message meta-data.
*
id
- The message id. *sent
- The date the message was sent (as aDate
instance). *timeStamp
- The date the message was received (in milliseconds). *peers
- An array of recipient ids. (This is empty for broadcasts.)
Subetha::protocol
The SemVer compatible version of the SubEtha protocol supported by this module.
Subetha::urls
Hash of urls keyed by an alias. This collection is used to resolved the client url when establishing a connection. The default members are:
local
: A JavaScript URL that loads a publicly hosted copy of Subetha.public
: A publicly hosted bridge.
Subetha::version
The SemVer compatible version of the Client module.
Installation
SubEtha-Client 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-Client isn't compatible with your favorite runtime, please file an issue or pull-request (preferred).
Dependencies
SubEtha-Client depends on the following modules:
SubEtha-Client also uses the following ECMAScript 5 and HTML 5 features:
You will need to implement shims for these browser features in unsupported environments. Note however that postMessage and localStorage shims will only allow this module to run without errors, not work as expected.
Web Browsers
Use a <SCRIPT>
tag to load the subetha-client.min.js file in your web page. (The file includes the module dependencies for your convenience.) Doing so, adds Subetha
to the global scope.
<script type="text/javascript" src="path/to/subetha-client.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-client
component install bemson/subetha-client
bower install subetha-client
AMD
Assuming you have a require.js compatible loader, configure an alias for the SubEtha-Client module (the term "subetha-client" is recommended, for consistency). The subetha-client module exports a module namespace.
require.config({
paths: {
'subetha-client': 'my/libs/subetha-client'
}
});
Then require and use the module in your application code:
require(['subetha-client'], function (Subetha) {
// ... SubEtha dependent code ...
});
Warning: Do not load the minified file via AMD, since it includes SubEtha-Client dependencies, which themselves export modules. Use AMD optimizers like r.js, in order to roll-up your dependency tree.
License
SubEtha is available under the terms of the Apache-License.
Copyright 2014, Bemi Faison