Skip to content

Instantly share code, notes, and snippets.

@snorpey
Created February 12, 2015 23:05
Show Gist options
  • Save snorpey/05ba1b0e932fb0ef0d10 to your computer and use it in GitHub Desktop.
Save snorpey/05ba1b0e932fb0ef0d10 to your computer and use it in GitHub Desktop.
Spacebrew Disconnect Test

Spacebrew Disconnect Test

  1. Clone Spacebrew
  2. npm install depencencies
  3. Run node node node_server_forever.js
  4. Download this test using the download button on the left and extract it
  5. Open index.html in the browser
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Admin</title>
<style>body{font-family:sans-serif;padding:50px;}.is-connected{background-color:green}</style>
</head>
<body>
<h1>ADMIN</h1>
<script src="spacebrew-1.4.1.js"></script>
<script src="sb-admin-0.1.4.js"></script>
<script>
/*global Spacebrew*/
var sb = new Spacebrew.Client( { server: 'localhost', name: 'Admin', reconnect: false } );
sb.extend(Spacebrew.Admin);
sb.onNewClient = function ( client ) {
if ( client.name === 'User' ) {
document.body.classList.add( 'is-connected' );
}
};
sb.onRemoveClient = function ( name ) {
if ( name === 'User' ) {
document.body.classList.remove( 'is-connected' );
}
};
sb.connect();
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>*{margin:0;padding:0;box-sizing:border-box;}iframe{width:50vw;height:100vh;border:none;display:block;float:left;}</style>
</head>
<body>
<iframe src="user.html"></iframe>
<iframe src="admin.html"></iframe>
</body>
</html>
/**
* Spacebrew Admin Mixin for Javascript
* --------------------------------
*
* This is an admin client mixim for the Spacebrew library. By passing the Spacebrew.Admin
* object into the Spacebrew.Client library's extend() method your client application will also
* connect to Spacebrew as an admin.
*
* Spacebrew is an open, dynamically re-routable software toolkit for choreographing interactive
* spaces. Or, in other words, a simple way to connect interactive things to one another. Learn
* more about Spacebrew here: http://docs.spacebrew.cc/
*
* Please note that this library only works will the Spacebrew.js library sb-1.0.4.min.js and
* above.
*
* Latest Updates:
* - enable client apps to be extended to include admin privileges.
* - added methods to handle admin messages and to update routes.
*
* @author Julio Terra and Brett Renfer
* @filename sb-admin-0.1.4.js
* @version 0.1.4
* @date April 8, 2013
*
*/
Spacebrew.Admin = {
admin: {
config: {
admin: true
, no_msgs: true
}
, active: true
, remoteAddress: undefined
, clients: []
, routes: []
}
}
/*********************************
** ADMIN CONFIGURATION METHODS
*********************************/
/**
* Sends the admin config message to the Spacebrew server. This message resgisters
* a client app as an admin app. This method is automatically called by the
* Spacebrew library if the library is extended with the admin mix-in before the
* client is configured.
*
* @memberOf Spacebrew.Admin
* @private
*/
Spacebrew.Admin.connectAdmin = function () {
this.socket.send(JSON.stringify(this.admin.config));
}
/**
* Enables turning on or off configuration option for admin app to receive all
* messages sent between clients. This option must be set before the
* Spacebrew server connection is made
*
* @param {Boolean} get_msgs Flag that sets admin message request on or off
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.getMsgs = function ( get_msgs ) {
if (get_msgs == false) {
config.no_msgs = false;
}
else {
config.no_msgs = true;
}
}
/*********************************
** EVENT CALLBACK METHODS STUBS
*********************************/
/**
* Override in your app to receive new client information, e.g. sb.onNewClient = yourFunction
* Admin-related method.
*
* @param {Object} client Object with client config details described below:
* - {String} name Name of client
* - {String} address IP address of client
* - {String} description Description of client
* - {Array} publish Array with all publish data feeds for client
* - {Array} subscribe Array with subscribe data feeds for client
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.onNewClient = function( client ){}
/**
* Override in your app to receive updated information about existing client, e.g. sb.onNewClient = yourFunction
* Admin-related method.
*
* @param {Object} client Object with client config details described below
* - {String} name Name of client
* - {String} address IP address of client
* - {String} description Description of client
* - {Array} publish Array with all publish data feeds for client
* - {Array} subscribe Array with subscribe data feeds for client
*
* @memberOf Spacebrew.Admin
* @public
*/
// Spacebrew.Admin.onUpdateClient = function( name, address, description, pubs, subs ){}
Spacebrew.Admin.onUpdateClient = function( client ){}
/**
* Override in your app to receive information about new routes, e.g. sb.onNewRoute = yourStringFunction
* Admin-related method.
*
* @param {String} action Type of action route message, either add or remove
* @param {Object} pub Object with name of client name and address, publish name and type
* @param {Object} sub Object with name of client name and address, subscribe name and type
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.onUpdateRoute = function( action, pub, sub ){}
/**
* Override in your app to receive client removal messages, e.g. sb.onCustomMessage = yourStringFunction
* Admin-related method.
*
* @param {String} name Name of client being removed
* @param {String} address Address of client being removed
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.onRemoveClient = function( name, address ){}
/**********************************
** PRIVATE EVENT HANDLER METHODS
**********************************/
/**
* Handles admin messages received from the Spacbrew server. Routes the messages based on the
* message type
* @param {Object} data Message object that contains an admin, config, remove, or route message
*
* @memberOf Spacebrew.Admin
* @private
*/
Spacebrew.Admin._handleAdminMessages = function( data ){
if (this.debug) console.log("[_handleAdminMessages] new message received ");
if (data["admin"]) {
// nothing to be done
}
else if (data["remove"]) {
if (this.debug) console.log("[_handleAdminMessages] remove client message ", data["remove"]);
for (var i = 0; i < data.remove.length; i ++) {
this._onRemoveClient( data.remove[i] );
}
}
else if (data["route"]) {
if (this.debug) console.log("[_handleAdminMessages] route update message ", data["route"]);
var new_route = {
route: {
type: data.route.type,
publisher: data.route.publisher,
subscriber: data.route.subscriber
}
}
if ( data.route.type !== "remove" ){
var bFound = false;
for (var i = 0; i < this.admin.routes.length; i++) {
// if route does not exists then create it, otherwise abort
if (this._compareRoutes(new_route.route, this.admin.routes[i].route)){
bFound = true;
break;
}
}
if ( !bFound ){
this.admin.routes.push(new_route);
}
} else {
for (var i = this.admin.routes.length - 1; i >= 0; i--) {
// if route exists then remove it, otherwise abort
if (this._compareRoutes(data.route, this.admin.routes[i].route)){
this.admin.routes.splice(i,i);
bFound = true;
break;
}
}
}
this.onUpdateRoute( data.route.type, data.route.publisher, data.route.subscriber );
}
else if (data instanceof Array || data["config"]) {
if (this.debug) console.log("[_handleAdminMessages] handle client config message(s) ", data);
if (data["config"]) data = [data];
for (var i = 0; i < data.length; i ++) {
if (data[i]["config"]) {
this._onNewClient(data[i].config);
}
}
}
}
/**
* Called when a new client message is received. It adds the client to this.admin.clients
* array and then forwards the message to front-end callback methods.
*
* @param {Object} client Object with client config details described below:
* - {String} name Name of client
* - {String} address IP address of client
* - {String} description Description of client
* - {Array} publish Array with all publish data feeds for client
* - {Array} subscribe Array with subscribe data feeds for client
*
* @memberOf Spacebrew.Admin
* @private
*/
Spacebrew.Admin._onNewClient = function( client ){
var existing_client = false;
this._setLocalIPAddress( client );
for( var j = 0; j < this.admin.clients.length; j++ ){
if ( this.admin.clients[j].name === client.name
&& this.admin.clients[j].remoteAddress === client.remoteAddress ) {
if (this.debug) console.log("existing client logged on " + client.name + " address " + client.remoteAddress);
existing_client = true;
this.admin.clients[j].publish = client.publish;
this.admin.clients[j].subscribe = client.subscribe;
this.admin.clients[j].description = client.description;
this.onUpdateClient( client );
}
}
//if we did not find a matching client, then add this one
if ( !existing_client ) {
if (this.debug) console.log("new client logged on " + client.name + " address " + client.remoteAddress);
this.admin.clients.push( client );
this.onNewClient( client );
}
}
Spacebrew.Admin._onRemoveClient = function( client ){
var existing_client = false;
var clientIndex = -1;
for( var j = 0; j < this.admin.clients.length; j++ ){
if ( this.admin.clients[j].name === client.name
&& this.admin.clients[j].remoteAddress === client.remoteAddress ) {
if (this.debug) console.log("existing client logged on " + client.name + " address " + client.remoteAddress);
existing_client = true;
clientIndex = j;
}
}
//if we found a matching client, remove it
if ( existing_client ) {
if (this.debug) console.log("removed client " + client.name + " address " + client.remoteAddress);
this.admin.clients.splice( clientIndex, 1 );
}
// broadcast event regardless
this.onRemoveClient( client.name, client.remoteAddress );
}
/**
* Checks new client config messages to capture the IP address of the local application.
* It first check if the current apps IP address has already been identified, if not
* then it confirms that app name, and all publishers and subscribers match.
*
* @param {Object} client Object with client config details described below:
* - {String} name Name of client
* - {String} address IP address of client
* - {String} description Description of client
* - {Array} publish Array with all publish data feeds for client
* - {Array} subscribe Array with subscribe data feeds for client
*
* @memberOf Spacebrew.Admin
* @private
*/
Spacebrew.Admin._setLocalIPAddress = function ( client ) {
var match_confirmed = true
, cur_pub_sub = ["subscribe", "publish"]
, client_config = []
, local_config = []
;
// check if client already exists
if (client.name === this._name && !this.admin.remoteAddress) {
if ((client.publish.messages.length == this.client_config.publish.messages.length) &&
(client.subscribe.messages.length == this.client_config.subscribe.messages.length)) {
for (var j = 0; j < cur_pub_sub.length; j ++ ) {
client_config = client[cur_pub_sub[j]].messages;
local_config = this.client_config[cur_pub_sub[j]].messages;
for (var i = 0; i < client_config.length; i ++ ) {
if (!(client_config[i].name === local_config[i].name) ||
!(client_config[i].type === local_config[i].type)) {
match_confirmed = false;
break;
}
}
}
if (match_confirmed){
this.admin.remoteAddress = client.remoteAddress;
if (this.debug) console.log("[_setLocalIPAddress] local IP address set to ", this.admin.remoteAddress);
}
}
}
}
/**********************************
** ROUTE HANDLER METHODS
**********************************/
/**
* Method that is used to add a route to the Spacebrew server
* @param {String or Object} pub_client Publish client app name OR
* object with all publish information.
* @param {String or Object} pub_address Publish app remote IP address OR
* object with all subscribe information.
* @param {String} pub_name Publish name
* @param {String} sub_client Subscribe client app name
* @param {String} sub_address Subscribe app remote IP address
* @param {String} sub_name Subscribe name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.addRoute = function ( pub_client, pub_address, pub_name, sub_client, sub_address, sub_name ){
this._updateRoute("add", pub_client, pub_address, pub_name, sub_client, sub_address, sub_name);
}
/**
* Method that is used to add a sub route to the Spacebrew server
* @param {String} pub_name Publish name
* @param {String} sub_client Subscribe client app name
* @param {String} sub_address Subscribe app remote IP address
* @param {String} sub_name Subscribe name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.addSubRoute = function ( pub_name, sub_client, sub_address, sub_name ){
if (!this.admin.remoteAddress) return;
this._updateRoute("add", this._name, this.admin.remoteAddress, pub_name, sub_client, sub_address, sub_name);
}
/**
* Method that is used to add a pub route to the Spacebrew server
* @param {String} sub_name Publish name
* @param {String} pub_client Subscribe client app name
* @param {String} pub_address Subscribe app remote IP address
* @param {String} pub_name Subscribe name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.addPubRoute = function ( sub_name, pub_client, pub_address, pub_name){
if (!this.admin.remoteAddress) return;
this._updateRoute("add", pub_client, pub_address, pub_name, this._name, this.admin.remoteAddress, sub_name);
}
/**
* Method that is used to remove a route from the Spacebrew server
* @param {String or Object} pub_client Publish client app name OR
* object with all publish information.
* @param {String or Object} pub_address Publish app remote IP address OR
* object with all subscribe information.
* @param {String} pub_name Publish name
* @param {String} sub_client Subscribe client app name
* @param {String} sub_address Subscribe app remote IP address
* @param {String} sub_name Subscribe name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.removeRoute = function ( pub_client, pub_address, pub_name, sub_client, sub_address, sub_name ){
this._updateRoute("remove", pub_client, pub_address, pub_name, sub_client, sub_address, sub_name);
}
/**
* Method that is used to remove a sub route from the Spacebrew server
* @param {String} pub_name Publish name
* @param {String} sub_client Subscribe client app name
* @param {String} sub_address Subscribe app remote IP address
* @param {String} sub_name Subscribe name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.removeSubRoute = function ( pub_name, sub_client, sub_address, sub_name ){
if (!this.admin.remoteAddress) return;
this._updateRoute("remove", this._name, this.admin.remoteAddress, pub_name, sub_client, sub_address, sub_name);
}
/**
* Method that is used to remove a pub route from the Spacebrew server
* @param {String} sub_name Publish name
* @param {String} pub_client Subscribe client app name
* @param {String} pub_address Subscribe app remote IP address
* @param {String} pub_name Subscribe name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.removePubRoute = function ( sub_name, pub_client, pub_address, pub_name){
if (!this.admin.remoteAddress) return;
this._updateRoute("remove", pub_client, pub_address, pub_name, this._name, this.admin.remoteAddress, sub_name);
}
/**
* Method that handles both add and remove route requests. Responsible for parsing requests
* and communicating with Spacebrew server
*
* @param {String} type Type of route request, either "add" or "remove"
* @param {String or Object} pub_client Publish client app name OR
* @param {String or Object} pub_client Publish client app name OR
* object with all publish information.
* @param {String or Object} pub_address Publish app remote IP address OR
* object with all subscribe information.
* @param {String} pub_name Publish name
* @param {String} sub_client Subscribe client app name
* @param {String} sub_address Subscribe app remote IP address
* @param {String} sub_name Subscribe name
*
* @memberOf Spacebrew.Admin
* @private
*/
Spacebrew.Admin._updateRoute = function ( type, pub_client, pub_address, pub_name, sub_client, sub_address, sub_name ){
var new_route
, route_type
, subscribe
, publish
;
// if request type is not supported then abort
if (type !== "add" && type !== "remove") return;
// check if pub and sub information was in first two arguments. If so then
if ((pub_client.clientName && pub_client.remoteAddress && pub_client.name && pub_client.type != undefined) &&
(pub_address.clientName && pub_address.remoteAddress && pub_address.name && pub_address.type != undefined)) {
new_route = {
route: {
type: type,
publisher: pub_client,
subscriber: pub_address
}
}
if (type === "add") {
var bFound = false;
for (var i = 0; i < this.admin.routes.length; i++) {
// if route does not exists then create it, otherwise abort
if (this._compareRoutes(new_route.route, this.admin.routes[i].route)){
bFound = true;
break;
}
}
if ( bFound ){
return;
} else {
this.admin.routes.push(new_route);
}
}
else if (type === "remove") {
var bFound = false;
for (var i = this.admin.routes.length - 1; i >= 0; i--) {
// if route exists then remove it, otherwise abort
if (this._compareRoutes(new_route.route, this.admin.routes[i].route)){
this.admin.routes.splice(i,i);
bFound = true;
break;
}
}
if ( !bFound ){
console.log("[_updateRoute] trying to remove route that does not exist");
//return;
}
}
// send new route information to spacebrew server
if (this.debug) console.log("[_updateRoute] sending route to admin ", JSON.stringify(new_route));
this.socket.send(JSON.stringify(new_route));
return;
}
pub_type = this.getPublishType(pub_client, pub_address, pub_name);
sub_type = this.getSubscribeType(sub_client, sub_address, sub_name);
if (pub_type != sub_type || pub_type == false || pub_type == undefined) {
if (this.debug) console.log("[_updateRoute] not routed :: types don't match - pub:" + pub_type + " sub: " + sub_type);
return;
}
publish = {
clientName: pub_client,
remoteAddress: pub_address,
name: pub_name,
type: pub_type
}
if (this.debug) console.log("[_updateRoute] created pub object ", publish);
subscribe = {
clientName: sub_client,
remoteAddress: sub_address,
name: sub_name,
type: sub_type
}
if (this.debug) console.log("[_updateRoute] created sub object ", subscribe);
// call itself with publish and subscribe objects properly formatted
this._updateRoute(type, publish, subscribe);
}
/**
* Compares two different routes.
*
* @param {Object} route_a Route description that contains client name and remote address,
* route name and type
* @param {Object} route_b Route description that contains client name and remote address,
* route name and type
* @return {Boolean} Returns true of match is valid, false otherwise
*
* @memberOf Spacebrew.Admin
* @private
*/
Spacebrew.Admin._compareRoutes = function (route_a, route_b){
// publisher
var bPublisherMatch = false;
if ((route_a.publisher.clientName === route_b.publisher.clientName) &&
(route_a.publisher.name === route_b.publisher.name) &&
(route_a.publisher.type === route_b.publisher.type) &&
(route_a.publisher.remoteAddress === route_b.publisher.remoteAddress)) {
bPublisherMatch = true;
}
// subscriber
var bSubscriberMatch = false;
if ((route_a.subscriber.clientName === route_b.subscriber.clientName) &&
(route_a.subscriber.name === route_b.subscriber.name) &&
(route_a.subscriber.type === route_b.subscriber.type) &&
(route_a.subscriber.remoteAddress === route_b.subscriber.remoteAddress)) {
bSubscriberMatch = true;
}
return bSubscriberMatch && bPublisherMatch;
}
/**********************************
** INSPECT CLIENT METHODS
**********************************/
/**
* Returns the type of a publisher
*
* @param {String} client_name Name of the client app
* @param {String} remote_address Remote address of client app
* @param {String} pub_name Publisher name
* @return {String} Data type name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.getPublishType = function (client_name, remote_address, pub_name){
return this._getPubSubType("publish", client_name, remote_address, pub_name);
}
/**
* Returns the type of a subscriber
*
* @param {String} client_name Name of the client app
* @param {String} remote_address Remote address of client app
* @param {String} pub_name Subscriber name
* @return {String} Data type name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.getSubscribeType = function (client_name, remote_address, sub_name){
return this._getPubSubType("subscribe", client_name, remote_address, sub_name);
}
/**
* Returns the type of a subscriber or publisher
*
* @param {String} pub_or_sub Flag that identifies if matching subscriber or publisher
* @param {String} client_name Name of the client app
* @param {String} remote_address Remote address of client app
* @param {String} pub_name Subscriber name
* @return {String} Data type name
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin._getPubSubType = function (pub_or_sub, client_name, remote_address, pub_sub_name){
var clients;
for( var j = 0; j < this.admin.clients.length; j++ ){
client = this.admin.clients[j];
if ( client.name === client_name && client.remoteAddress === remote_address ) {
for( var i = 0; i < client[pub_or_sub].messages.length; i++ ){
//if (this.debug) console.log("Compare Types " + client[pub_or_sub].messages[i].name + " with " + pub_sub_name)
if (client[pub_or_sub].messages[i].name === pub_sub_name) {
return client[pub_or_sub].messages[i].type;
}
}
}
}
return false;
}
/**
* Checks if the a client name and ip address refer to current app
*
* @param {String} client_name Name of the client
* @param {String} remote_address IP address of the client
* @return {Boolean} True if name and ip address refers to current app
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.isThisApp = function (client_name, remote_address){
if (this._name === client_name && this.admin.remoteAddress === remote_address) return true;
else return false;
}
/**
* Returns the client that matches the name and remoteAddress parameters queried.
*
* @param {String} name Name of the client application
* @param {String} remoteAddress IP address of the client apps
* @return {Object} Object featuring all client config information
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.getClient = function (name, remoteAddress){
var client;
for( var j = 0; j < this.admin.clients.length; j++ ){
client = this.admin.clients[j];
if ( client.name === name && client.remoteAddress === remoteAddress ) {
return client;
}
}
}
/**********************************
** UTILITY METHODS
**********************************/
/**
* returns a list of all subscribers that match a specific type.
* @param {String} type Data type of subscribers that should be returned
* @return {Array} Array with subscribers
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.subscribeListByType = function (type){
return this._pubSubByType("subscribe", type);
}
/**
* returns a list of all publishers that match a specific type.
* @param {String} type Data type of publishers that should be returned
* @return {Array} Array with publishers
*
* @memberOf Spacebrew.Admin
* @public
*/
Spacebrew.Admin.publishListByType = function (type){
return this._pubSubByType("publish", type);
}
/**
* returns a list of all publishers or subscribers that match a specific type.
* @param {String} pub_or_sub Flag that identifies if method should return publishers or subscribers
* @param {String} type Data type of publishers or subscribers that should be returned
* @return {Array} Array with publishers
*
* @memberOf Spacebrew.Admin
* @private
*/
Spacebrew.Admin._pubSubByType = function (pub_or_sub, type){
var client = {}
, filtered_clients = []
, pub_sub_item = {}
, new_item = {}
;
for( var j = 0; j < this.admin.clients.length; j++ ){
client = this.admin.clients[j];
for (var i = 0; i < client[pub_or_sub].messages.length; i++) {
pub_sub_item = client[pub_or_sub].messages[i];
if ( pub_sub_item.type === type ) {
new_item = { clientName: client.name
, remoteAddress: client.remoteAddress
, name: pub_sub_item.name
, type: pub_sub_item.type
};
filtered_clients.push( new_item );
}
}
}
return filtered_clients;
}
/**
*
* Spacebrew Library for Javascript
* --------------------------------
*
* This library was designed to work on front-end (browser) envrionments, and back-end (server)
* environments. Please refer to the readme file, the documentation and examples to learn how to
* use this library.
*
* Spacebrew is an open, dynamically re-routable software toolkit for choreographing interactive
* spaces. Or, in other words, a simple way to connect interactive things to one another. Learn
* more about Spacebrew here: http://docs.spacebrew.cc/
*
* To import into your web apps, we recommend using the minimized version of this library.
*
* Latest Updates:
* - added binary message support
* - added blank "options" attribute to config message - for future use
* - caps number of messages sent to 60 per second
* - reconnect to spacebrew if connection lost
* - enable client apps to extend libs with admin functionality.
* - added close method to close Spacebrew connection.
*
* @author LAB at Rockwell Group, Brett Renfer, Eric Eckhard-Ishii, Julio Terra, Quin Kennedy
* @filename sb-1.4.1.js
* @version 1.4.1
* @date April 8, 2014
*
*/
/**
* Check if Bind method exists in current enviroment. If not, it creates an implementation of
* this useful method.
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP
? this
: oThis || window,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
/**
* @namespace for Spacebrew library
*/
var Spacebrew = Spacebrew || {};
/**
* create placeholder var for WebSocket object, if it does not already exist
*/
var WebSocket = WebSocket || {};
/**
* Check if Running in Browser or Server (Node) Environment *
*/
// check if window object already exists to determine if running browswer
var window = window || undefined;
// check if module object already exists to determine if this is a node application
var module = module || undefined;
// if app is running in a browser, then define the getQueryString method
if (window) {
if (!window["getQueryString"]){
/**
* Get parameters from a query string
* @param {String} name Name of query string to parse (w/o '?' or '&')
* @return {String} value of parameter (or empty string if not found)
*/
window.getQueryString = function( name ) {
if (!window.location){
return;
}
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
var regexS = "[\\?&]"+name+"=([^&#]*)";
var regex = new RegExp( regexS );
var results = regex.exec( window.location.href );
if( results == null ){
return "";
} else {
return results[1];
}
};
}
}
// if app is running in a node server environment then package Spacebrew library as a module.
// WebSocket module (ws) needs to be saved in a node_modules so that it can be imported.
if (!window && module) {
WebSocket = require("ws");
module.exports = {
Spacebrew: Spacebrew
};
}
/**
* Define the Spacebrew Library *
*/
/**
* Spacebrew client!
* @constructor
* @param {String} server (Optional) Base address of Spacebrew server. This server address is overwritten if server defined in query string; defaults to localhost.
* @param {String} name (Optional) Base name of app. Base name is overwritten if "name" is defined in query string; defaults to window.location.href.
* @param {String} description (Optional) Base description of app. Description name is overwritten if "description" is defined in query string;
* @param {Object} options (Optional) An object that holds the optional parameters described below
* port (Optional) Port number for the Spacebrew server
* admin (Optional) Flag that identifies when app should register for admin privileges with server
* debug (Optional) Debug flag that turns on info and debug messaging (limited use)
*/
Spacebrew.Client = function( server, name, description, options ){
options = options || {};
// check if the server variable is an object that holds all config values
if (server !== undefined) {
if (toString.call(server) !== "[object String]") {
options.port = server.port || undefined;
options.debug = server.debug || false;
options.reconnect = server.reconnect || false;
description = server.description || undefined;
name = server.name || undefined;
server = server.server || undefined;
}
}
this.debug = false;
if ( window ){
this.debug = (window.getQueryString("debug") === "true" ? true : (options.debug || false));
}
this.reconnect = typeof options.reconnect === "boolean" ? options.reconnect : true;
this.reconnect_timer = undefined;
this.sendRateCapped = options.capSendRate === undefined ? false : options.capSendRate;
this.send_interval = options.sendRate || 16;
this.send_blocked = false;
this.msg = {};
/**
* Name of app
* @type {String}
*/
this._name = name || "javascript client #";
if (window) {
this._name = (window.getQueryString("name") !== "" ? unescape(window.getQueryString("name")) : this._name);
}
/**
* Description of your app
* @type {String}
*/
this._description = description || "spacebrew javascript client";
if (window) {
this._description = (window.getQueryString("description") !== "" ? unescape(window.getQueryString("description")) : this._description);
}
/**
* Spacebrew server to which the app will connect
* @type {String}
*/
this.server = server || "sandbox.spacebrew.cc";
if (window) {
this.server = (window.getQueryString("server") !== "" ? unescape(window.getQueryString("server")) : this.server);
}
/**
* Port number on which Spacebrew server is running
* @type {Integer}
*/
this.port = options.port || 9000;
if (window) {
var port = window.getQueryString("port");
if (port !== "" && !isNaN(port)) {
this.port = port;
}
}
/**
* Reference to WebSocket
* @type {WebSocket}
*/
this.socket = null;
/**
* Configuration file for Spacebrew
* @type {Object}
*/
this.client_config = {
name: this._name,
description: this._description,
publish:{
messages:[]
},
subscribe:{
messages:[]
},
options:{}
};
this.admin = {};
/**
* Are we connected to a Spacebrew server?
* @type {Boolean}
*/
this._isConnected = false;
};
/**
* Connect to Spacebrew
* @memberOf Spacebrew.Client
*/
Spacebrew.Client.prototype.connect = function(){
try {
this.socket = new WebSocket("ws://" + this.server + ":" + this.port);
this.socket.binaryType = "arraybuffer";
this.socket.onopen = this._onOpen.bind(this);
this.socket.onmessage = this._onMessage.bind(this);
this.socket.onclose = this._onClose.bind(this);
} catch(e){
this._isConnected = false;
console.log("[connect:Spacebrew] connection attempt failed");
}
};
/**
* Close Spacebrew connection
* @memberOf Spacebrew.Client
*/
Spacebrew.Client.prototype.close = function(){
try {
if (this._isConnected) {
this.socket.close();
this._isConnected = false;
console.log("[close:Spacebrew] closing websocket connection");
}
} catch (e) {
this._isConnected = false;
}
};
/**
* Override in your app to receive on open event for connection
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.onOpen = function( name, value ){};
/**
* Override in your app to receive on close event for connection
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.onClose = function( name, value ){};
/**
* Override in your app to receive "range" messages, e.g. sb.onRangeMessage = yourRangeFunction
* @param {String} name Name of incoming route
* @param {Number} value The data being provided
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.onRangeMessage = function( name, value ){};
/**
* Override in your app to receive "boolean" messages, e.g. sb.onBooleanMessage = yourBoolFunction
* @param {String} name Name of incoming route
* @param {Boolean} value The data being provided
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.onBooleanMessage = function( name, value ){};
/**
* Override in your app to receive "string" messages, e.g. sb.onStringMessage = yourStringFunction
* @param {String} name Name of incoming route
* @param {String} value The data being provided
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.onStringMessage = function( name, value ){};
/**
* Override in your app to receive "custom" messages, e.g. sb.onCustomMessage = yourStringFunction
* @param {String} name Name of incoming route
* @param {String} value The data being provided
* @param {String} type The type name of this route
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.onCustomMessage = function( name, value, type ){};
/**
* Override in your app to receive any binary messages, e.g. sb.onBinaryMessage = yourBinaryFunction
* @param {String} name Name of incoming route
* @param {Object} value The data being provided on the named route {buffer:[received ArrayBuffer], startIndex:[start index for binary data]}
* @param {String} type The type name of this route
*/
Spacebrew.Client.prototype.onBinaryMessage = function( name, value, type ){};
/**
* Add a route you are publishing on
* @param {String} name Name of incoming route
* @param {String} type "boolean", "range", or "string"
* @param {String} def default value
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.addPublish = function( name, type, def ){
this.client_config.publish.messages.push({"name":name, "type":type, "default":def});
this.updatePubSub();
};
/**
* [addSubscriber description]
* @param {String} name Name of outgoing route
* @param {String} type "boolean", "range", or "string"
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.addSubscribe = function( name, type ){
this.client_config.subscribe.messages.push({"name":name, "type":type });
this.updatePubSub();
};
/**
* Update publishers and subscribers
* @memberOf Spacebrew.Client
* @private
*/
Spacebrew.Client.prototype.updatePubSub = function(){
if (this._isConnected) {
this.socket.send(JSON.stringify({"config": this.client_config}));
}
};
/**
* Send a route to Spacebrew
* @param {String} name Name of outgoing route (must match something in addPublish)
* @param {String} type "boolean", "range", or "string"
* @param {String} value Value to send
* @memberOf Spacebrew.Client
* @public
*/
Spacebrew.Client.prototype.send = function( name, type, value ){
var self = this;
this.msg = {
"message": {
"clientName":this._name,
"name": name,
"type": type
}
};
if (typeof(value) == "string"){
this.msg.message.value = value;
} else {
if (("buffer" in value) && (value.buffer instanceof ArrayBuffer)){
value = value.buffer;
}
if (value instanceof ArrayBuffer){
this.msg.message.value = value.byteLength;
} else {
//unexpected value type
return;
}
}
// are we capping the rate at which we send messages?
if ( this.sendRateCapped ){
// if send block is not active then send message
if (!this.send_blocked) {
this.socket.send(JSON.stringify(this.msg));
this.send_blocked = true;
this.msg = undefined;
// set the timer to unblock message sending
setTimeout(function() {
self.send_blocked = false; // remove send block
if (self.msg !== undefined) { // if message exists then sent it
self.send(self.msg.message.name, self.msg.message.type, self.msg.message.value);
}
}, self.send_interval);
}
} else {
if (value instanceof ArrayBuffer){
var jsonString = JSON.stringify(this.msg);
var jsonByteLength = Spacebrew.numBytesToEncodeString(jsonString);
var numBytesForJsonLength = (jsonByteLength < 254 ? 1 : (jsonByteLength <= 0xFFFF ? 3 : 5));
var bufferSize = value.byteLength + jsonByteLength + numBytesForJsonLength;
var newBuffer = new ArrayBuffer( bufferSize );
var numView = new Uint8Array(newBuffer, 0, numBytesForJsonLength);
if (numBytesForJsonLength == 5){
numView[0] = 255;
numView[1] = ((jsonByteLength >>> 24) & 0xFF);
numView[2] = ((jsonByteLength >>> 16) & 0xFF);
numView[3] = ((jsonByteLength >>> 8) & 0xFF);
numView[4] = (jsonByteLength & 0xFF);
} else if (numBytesForJsonLength == 3){
numView[0] = 254;
numView[1] = ((jsonByteLength >>> 8) & 0xFF);
numView[2] = (jsonByteLength & 0xFF);
} else {
numView[0] = jsonByteLength;
}
Spacebrew.encodeToBuffer(jsonString, new Uint8Array(newBuffer, numBytesForJsonLength, jsonByteLength));
var dataView = new Uint8Array(newBuffer, numBytesForJsonLength + jsonByteLength);
dataView.set(new Uint8Array(value));
this.socket.send(newBuffer);
} else {
this.socket.send(JSON.stringify(this.msg));
}
}
};
/**
* Called on WebSocket open
* @private
* @memberOf Spacebrew.Client
*/
Spacebrew.Client.prototype._onOpen = function() {
console.log("[_onOpen:Spacebrew] Spacebrew connection opened, client name is: " + this._name);
this._isConnected = true;
if (this.admin.active) {
this.connectAdmin();
}
// if reconnect functionality is activated then clear interval timer when connection succeeds
if (this.reconnect_timer) {
console.log("[_onOpen:Spacebrew] tearing down reconnect timer");
this.reconnect_timer = clearInterval(this.reconnect_timer);
this.reconnect_timer = undefined;
}
// send my config
this.updatePubSub();
this.onOpen();
};
/**
* Called on WebSocket message
* @private
* @param {Object} e
* @memberOf Spacebrew.Client
*/
Spacebrew.Client.prototype._onMessage = function( e ){
var data, binaryData;
if (e.data instanceof ArrayBuffer){
var binaryPacket = new Uint8Array(e.data);
var jsonLength = binaryPacket[0];
var jsonStartIndex = 1;
if (jsonLength == 254){
jsonLength = ((binaryPacket[1] << 8) + binaryPacket[2]);
jsonStartIndex = 3;
} else if (jsonLength == 255){
jsonLength = ((binaryPacket[1] << 24) + (binaryPacket[2] << 16) + (binaryPacket[3] << 8) + binaryPacket[4]);
jsonStartIndex = 5;
}
if (jsonLength > 0){
try {
var jsonString = Spacebrew.decodeFromBuffer(new Uint8Array(e.data, jsonStartIndex, jsonLength));
data = JSON.parse(jsonString);
binaryData = {"buffer":e.data, "startIndex":jsonStartIndex+jsonLength};
} catch (err){
console.error(err);
return;
}
} else {
//empty message?
return;
}
} else {
data = JSON.parse(e.data);
}
var name
, type
, value
, clientName // not used yet, needs to be added to callbacks!
;
// handle client messages
if ((!("targetType" in data) && !(data instanceof Array)) || data["targetType"] == "client"){
//expecting only messages
if ("message" in data) {
name = data.message.name;
type = data.message.type;
value = data.message.value;
// for now only adding this if we have it, for backwards compatibility
if ( data.message.clientName ) {
clientName = data.message.clientName;
}
if (binaryData !== undefined){
this.onBinaryMessage( name, binaryData, type );
} else {
switch( type ){
case "boolean":
this.onBooleanMessage( name, value == "true" );
break;
case "string":
this.onStringMessage( name, value );
break;
case "range":
this.onRangeMessage( name, Number(value) );
break;
default:
this.onCustomMessage( name, value, type );
}
}
} else {
//illegal message
return;
}
}
// handle admin messages
else {
if (this.admin) {
this._handleAdminMessages( data );
}
}
};
/**
* Called on WebSocket close
* @private
* @memberOf Spacebrew.Client
*/
Spacebrew.Client.prototype._onClose = function() {
var self = this;
console.log("[_onClose:Spacebrew] Spacebrew connection closed");
this._isConnected = false;
if (this.admin.active) {
this.admin.remoteAddress = undefined;
}
// if reconnect functionality is activated set interval timer if connection dies
if (this.reconnect && !this.reconnect_timer) {
console.log("[_onClose:Spacebrew] setting up reconnect timer");
this.reconnect_timer = setInterval(function () {
if (self.isConnected !== false) {
self.connect();
console.log("[reconnect:Spacebrew] attempting to reconnect to spacebrew");
}
}, 5000);
}
this.onClose();
};
/**
* name Method that sets or gets the spacebrew app name. If parameter is provided then it sets the name, otherwise
* it just returns the current app name.
* @param {String} newName New name of the spacebrew app
* @return {String} Returns the name of the spacebrew app if called as a getter function. If called as a
* setter function it will return false if the method is called after connecting to spacebrew,
* because the name must be configured before connection is made.
*/
Spacebrew.Client.prototype.name = function (newName){
if (newName) { // if a name has been passed in then update it
if (this._isConnected) {
return false; // if already connected we can't update name
}
this._name = newName;
if (window) {
this._name = (window.getQueryString("name") !== "" ? unescape(window.getQueryString("name")) : this._name);
}
this.client_config.name = this._name; // update spacebrew config file
}
return this._name;
};
/**
* name Method that sets or gets the spacebrew app description. If parameter is provided then it sets the description,
* otherwise it just returns the current app description.
* @param {String} newDesc New description of the spacebrew app
* @return {String} Returns the description of the spacebrew app if called as a getter function. If called as a
* setter function it will return false if the method is called after connecting to spacebrew,
* because the description must be configured before connection is made.
*/
Spacebrew.Client.prototype.description = function (newDesc){
if (newDesc) { // if a description has been passed in then update it
if (this._isConnected) {
return false; // if already connected we can't update description
}
this._description = newDesc || "spacebrew javascript client";
if (window) {
this._description = (window.getQueryString("description") !== "" ? unescape(window.getQueryString("description")) : this._description);
}
this.client_config.description = this._description; // update spacebrew config file
}
return this._description;
};
/**
* isConnected Method that returns current connection state of the spacebrew client.
* @return {Boolean} Returns true if currently connected to Spacebrew
*/
Spacebrew.Client.prototype.isConnected = function (){
return this._isConnected;
};
Spacebrew.Client.prototype.extend = function ( mixin ) {
for (var prop in mixin) {
if (mixin.hasOwnProperty(prop)) {
this[prop] = mixin[prop];
}
}
};
/**
* Returns the number of bytes necessary to encode the provided string in UTF-8
* @param {String} string The text to encode
* @return {Number} The number of bytes necessary to encode the provided text in UTF-8
*/
Spacebrew.numBytesToEncodeString = function( text ){
var totalBytes = 0;
for (var i = text.length - 1; i >= 0; i--) {
totalBytes += Spacebrew.numBytesToEncodeCode(text.charCodeAt(i));
}
return totalBytes;
};
/**
* Returns the number of bytes required to encode the given character code in UTF-8
* @param {Number} charCode The character code to encode
* @return {Number} The number of bytes required to encode in UTF-8
*/
Spacebrew.numBytesToEncodeCode = function( charCode ){
if ((charCode >> 16) > 0){
return 4;
}
if ((charCode >> 11) > 0){
return 3;
}
if ((charCode >> 7) > 0){
return 2;
}
return 1;
};
/**
* Encodes the provided string into an ArrayBuffer using UTF-8
* @param {String} string The string to encode into the ArrayBuffer
* @param {Uint8Array} buffer Optionally, pass in a buffer to use for storing the encoded bytes in.
* @return {Uint8Array} A Uint8Array containing the UTF-8 encoded bytes for the supplied string
*/
Spacebrew.encodeToBuffer = function( text, view ){
if (!(view instanceof Uint8Array)){
var buffer = new ArrayBuffer(Spacebrew.numBytesToEncodeString(text));
view = new Uint8Array(buffer);
}
var numBytes, charCode, currIndex = 0;
for (var i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
numBytes = Spacebrew.numBytesToEncodeCode(charCode);
if (numBytes == 1){
view[currIndex] = charCode;
currIndex++;
} else {
var temp = 0x80;
for(var j = 1; j < numBytes; j++){
temp = ((temp >> 1) | 0x80);
view[currIndex + numBytes - j] = (0x80 | (charCode & 0x3F));
charCode >>>= 6;
}
view[currIndex] = (temp | charCode);
currIndex += numBytes;
}
}
return view;
};
/**
* Decodes the provided Uint8Array into a string, assuming UTF-8 encoding
* @param {Uint8Array} buffer The encoded string
* @return {String} The decoded string
*/
Spacebrew.decodeFromBuffer = function( buffer ){
if (!(buffer instanceof Uint8Array)){
return undefined;
}
var charCode, numBytes, rawByte, text = "";
for(var i = 0; i < buffer.length;){
rawByte = buffer[i];
i++;
numBytes = 0;
while((rawByte & 0x80) > 0){
numBytes++;
rawByte = ((rawByte << 1) & 0xFF);
}
charCode = (rawByte >>> numBytes);
if (numBytes === 0){
numBytes = 1;
}
for(var j = 1; j < numBytes; j++){
charCode = ((charCode << 6) | (buffer[i] & 0x3F));
i++;
}
text += String.fromCharCode(charCode);
}
return text;
};
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>User</title>
<style>body{font-family:sans-serif;padding:50px;}.is-connected{background-color:green}</style>
</head>
<body>
<h1>USER</h1>
<label for="toggle"><input id="toggle" type="checkbox">toggle spacebrew connection</label>
<script src="spacebrew-1.4.1.js"></script>
<script>
/*global Spacebrew*/
var checkboxEl = document.getElementById( 'toggle' );
var sb = new Spacebrew.Client( { server: 'localhost', name: 'User', reconnect: false } );
sb.onOpen = function() {
checkboxEl.setAttribute( 'checked', 'checked' );
document.body.classList.add( 'is-connected' );
};
sb.onClose = function() {
checkboxEl.removeAttribute( 'checked' );
document.body.classList.remove( 'is-connected' );
};
checkboxEl.addEventListener( 'change', connectionToggled );
function connectionToggled () {
if ( checkboxEl.getAttribute( 'checked' ) ) {
sb.close();
} else {
sb.connect();
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment