Skip to content

Instantly share code, notes, and snippets.

@stevro
Forked from Maclay74/autobanh.js
Created February 17, 2023 21:44
Show Gist options
  • Select an option

  • Save stevro/59e8863e9e9a1b87369beeab09c2fb61 to your computer and use it in GitHub Desktop.

Select an option

Save stevro/59e8863e9e9a1b87369beeab09c2fb61 to your computer and use it in GitHub Desktop.

Revisions

  1. @Maclay74 Maclay74 created this gist Feb 17, 2023.
    1,073 changes: 1,073 additions & 0 deletions autobanh.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1073 @@
    import * as when from 'when';

    /** @license MIT License (c) 2011-2013 Copyright Tavendo GmbH. */

    /**
    * AutobahnJS - http://autobahn.ws
    *
    * A lightweight implementation of
    *
    * WAMP (The WebSocket Application Messaging Protocol) - http://wamp.ws
    *
    * Provides asynchronous RPC/PubSub over WebSocket.
    *
    * Copyright (C) 2011-2014 Tavendo GmbH. Licensed under the MIT License.
    * See license text at http://www.opensource.org/licenses/mit-license.php
    */

    /* global console: false, MozWebSocket: false, when: false, CryptoJS: false */

    /**
    * @define {string}
    */
    var AUTOBAHNJS_VERSION = '?.?.?';
    var global = this;

    (function (root, factory) {
    if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['when'], function (when) {
    // Also create a global in case some scripts
    // that are loaded still are looking for
    // a global even when an AMD loader is in use.
    return (root.ab = factory(root, when));
    });
    } else if (typeof exports !== 'undefined') {
    // Support Node.js specific `module.exports` (which can be a function)
    if (typeof module != 'undefined' && module.exports) {
    exports = module.exports = factory(root, root.when);
    }
    // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function)
    //exports.ab = exports;
    } else {
    // Browser globals
    root.ab = factory(root, root.when);
    }
    })(global, function (root, when) {
    'use strict';

    var ab = {};
    ab._version = AUTOBAHNJS_VERSION;

    // Helper to slice out browser / version from userAgent
    ab._sliceUserAgent = function (str, delim, delim2) {
    var ver = [];
    var ua = navigator.userAgent;
    var i = ua.indexOf(str);
    var j = ua.indexOf(delim, i);
    if (j < 0) {
    j = ua.length;
    }
    var agent = ua.slice(i, j).split(delim2);
    var v = agent[1].split('.');
    for (var k = 0; k < v.length; ++k) {
    ver.push(parseInt(v[k], 10));
    }
    return {name: agent[0], version: ver};
    };

    /**
    * Detect browser and browser version.
    */
    ab.getBrowser = function () {
    var ua = navigator.userAgent;
    if (ua.indexOf('Chrome') > -1) {
    return ab._sliceUserAgent('Chrome', ' ', '/');
    } else if (ua.indexOf('Safari') > -1) {
    return ab._sliceUserAgent('Safari', ' ', '/');
    } else if (ua.indexOf('Firefox') > -1) {
    return ab._sliceUserAgent('Firefox', ' ', '/');
    } else if (ua.indexOf('MSIE') > -1) {
    return ab._sliceUserAgent('MSIE', ';', ' ');
    } else {
    return null;
    }
    };

    ab.getServerUrl = function (wsPath, fallbackUrl) {
    if (root.location.protocol === 'file:') {
    if (fallbackUrl) {
    return fallbackUrl;
    } else {
    return 'ws://127.0.0.1/ws';
    }
    } else {
    var scheme = root.location.protocol === 'https:' ? 'wss://' : 'ws://';
    var port = root.location.port !== '' ? ':' + root.location.port : '';
    var path = wsPath ? wsPath : 'ws';
    return scheme + root.location.hostname + port + '/' + path;
    }
    };

    // Logging message for unsupported browser.
    ab.browserNotSupportedMessage = 'Browser does not support WebSockets (RFC6455)';

    // PBKDF2-base key derivation function for salted WAMP-CRA
    ab.deriveKey = function (secret, extra) {
    if (extra && extra.salt) {
    var salt = extra.salt;
    var keylen = extra.keylen || 32;
    var iterations = extra.iterations || 10000;
    var key = CryptoJS.PBKDF2(secret, salt, {
    keySize: keylen / 4,
    iterations: iterations,
    hasher: CryptoJS.algo.SHA256,
    });
    return key.toString(CryptoJS.enc.Base64);
    } else {
    return secret;
    }
    };

    ab._idchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    ab._idlen = 16;
    ab._subprotocol = 'wamp';

    ab._newid = function () {
    var id = '';
    for (var i = 0; i < ab._idlen; i += 1) {
    id += ab._idchars.charAt(Math.floor(Math.random() * ab._idchars.length));
    }
    return id;
    };

    ab._newidFast = function () {
    return Math.random().toString(36);
    };

    ab.log = function () {
    //console.log.apply(console, !!arguments.length ? arguments : [this]);
    if (arguments.length > 1) {
    console.group('Log Item');
    for (var i = 0; i < arguments.length; i += 1) {
    console.log(arguments[i]);
    }
    console.groupEnd();
    } else {
    console.log(arguments[0]);
    }
    };

    ab._debugrpc = false;
    ab._debugpubsub = false;
    ab._debugws = false;
    ab._debugconnect = false;

    ab.debug = function (debugWamp, debugWs, debugConnect) {
    if ('console' in root) {
    ab._debugrpc = debugWamp;
    ab._debugpubsub = debugWamp;
    ab._debugws = debugWs;
    ab._debugconnect = debugConnect;
    } else {
    throw 'browser does not support console object';
    }
    };

    ab.version = function () {
    return ab._version;
    };

    ab.PrefixMap = function () {
    var self = this;
    self._index = {};
    self._rindex = {};
    };

    ab.PrefixMap.prototype.get = function (prefix) {
    var self = this;
    return self._index[prefix];
    };

    ab.PrefixMap.prototype.set = function (prefix, uri) {
    var self = this;
    self._index[prefix] = uri;
    self._rindex[uri] = prefix;
    };

    ab.PrefixMap.prototype.setDefault = function (uri) {
    var self = this;
    self._index[''] = uri;
    self._rindex[uri] = '';
    };

    ab.PrefixMap.prototype.remove = function (prefix) {
    var self = this;
    var uri = self._index[prefix];
    if (uri) {
    delete self._index[prefix];
    delete self._rindex[uri];
    }
    };

    ab.PrefixMap.prototype.resolve = function (curie, pass) {
    var self = this;

    // skip if not a CURIE
    var i = curie.indexOf(':');
    if (i >= 0) {
    var prefix = curie.substring(0, i);
    if (self._index[prefix]) {
    return self._index[prefix] + curie.substring(i + 1);
    }
    }

    // either pass-through or null
    if (pass === true) {
    return curie;
    } else {
    return null;
    }
    };

    ab.PrefixMap.prototype.shrink = function (uri, pass) {
    var self = this;

    for (var i = uri.length; i > 0; i -= 1) {
    var u = uri.substring(0, i);
    var p = self._rindex[u];
    if (p) {
    return p + ':' + uri.substring(i);
    }
    }

    // either pass-through or null
    if (pass === true) {
    return uri;
    } else {
    return null;
    }
    };

    ab._MESSAGE_TYPEID_WELCOME = 0;
    ab._MESSAGE_TYPEID_PREFIX = 1;
    ab._MESSAGE_TYPEID_CALL = 2;
    ab._MESSAGE_TYPEID_CALL_RESULT = 3;
    ab._MESSAGE_TYPEID_CALL_ERROR = 4;
    ab._MESSAGE_TYPEID_SUBSCRIBE = 5;
    ab._MESSAGE_TYPEID_UNSUBSCRIBE = 6;
    ab._MESSAGE_TYPEID_PUBLISH = 7;
    ab._MESSAGE_TYPEID_EVENT = 8;

    ab.CONNECTION_CLOSED = 0;
    ab.CONNECTION_LOST = 1;
    ab.CONNECTION_RETRIES_EXCEEDED = 2;
    ab.CONNECTION_UNREACHABLE = 3;
    ab.CONNECTION_UNSUPPORTED = 4;
    ab.CONNECTION_UNREACHABLE_SCHEDULED_RECONNECT = 5;
    ab.CONNECTION_LOST_SCHEDULED_RECONNECT = 6;

    //ab.Deferred = when.defer;
    //ab.Deferred = jQuery.Deferred;

    ab.Deferred = () => {
    const deferred = {};
    const promise = new Promise(function (resolve, reject) {
    deferred.resolve = resolve;
    deferred.reject = reject;
    });
    deferred.promise = promise;
    return deferred;
    };

    ab._construct = function (url, protocols, token) {
    return new WebSocket(url, protocols, {headers: {Authorization: token}});
    };

    ab.Session = function (wsuri, onopen, onclose, options) {
    var self = this;

    self._wsuri = wsuri;
    self._options = options;
    self._websocket_onopen = onopen;
    self._websocket_onclose = onclose;

    self._websocket = null;
    self._websocket_connected = false;

    self._session_id = null;
    self._wamp_version = null;
    self._server = null;

    self._calls = {};
    self._subscriptions = {};
    self._prefixes = new ab.PrefixMap();

    self._txcnt = 0;
    self._rxcnt = 0;

    if (self._options && self._options.skipSubprotocolAnnounce) {
    self._websocket = ab._construct(self._wsuri, null, options.token);
    } else {
    self._websocket = ab._construct(self._wsuri, [ab._subprotocol], options.token);
    }

    if (!self._websocket) {
    if (onclose !== undefined) {
    onclose(ab.CONNECTION_UNSUPPORTED);
    return;
    } else {
    throw ab.browserNotSupportedMessage;
    }
    }

    self._websocket.onmessage = function (e) {
    if (ab._debugws) {
    self._rxcnt += 1;
    console.group('WS Receive');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(self._rxcnt);
    console.log(e.data);
    console.groupEnd();
    }

    var o = JSON.parse(e.data);
    if (o[1] in self._calls) {
    if (o[0] === ab._MESSAGE_TYPEID_CALL_RESULT) {
    var dr = self._calls[o[1]];
    var r = o[2];

    if (ab._debugrpc && dr._ab_callobj !== undefined) {
    console.group('WAMP Call', dr._ab_callobj[2]);
    console.timeEnd(dr._ab_tid);
    console.group('Arguments');
    for (var i = 3; i < dr._ab_callobj.length; i += 1) {
    var arg = dr._ab_callobj[i];
    if (arg !== undefined) {
    console.log(arg);
    } else {
    break;
    }
    }
    console.groupEnd();
    console.group('Result');
    console.log(r);
    console.groupEnd();
    console.groupEnd();
    }

    dr.resolve(r);
    } else if (o[0] === ab._MESSAGE_TYPEID_CALL_ERROR) {
    var de = self._calls[o[1]];
    var uri_ = o[2];
    var desc_ = o[3];
    var detail_ = o[4];

    if (ab._debugrpc && de._ab_callobj !== undefined) {
    console.group('WAMP Call', de._ab_callobj[2]);
    console.timeEnd(de._ab_tid);
    console.group('Arguments');
    for (var j = 3; j < de._ab_callobj.length; j += 1) {
    var arg2 = de._ab_callobj[j];
    if (arg2 !== undefined) {
    console.log(arg2);
    } else {
    break;
    }
    }
    console.groupEnd();
    console.group('Error');
    console.log(uri_);
    console.log(desc_);
    if (detail_ !== undefined) {
    console.log(detail_);
    }
    console.groupEnd();
    console.groupEnd();
    }

    if (detail_ !== undefined) {
    de.reject({uri: uri_, desc: desc_, detail: detail_});
    } else {
    de.reject({uri: uri_, desc: desc_});
    }
    }
    delete self._calls[o[1]];
    } else if (o[0] === ab._MESSAGE_TYPEID_EVENT) {
    var subid = self._prefixes.resolve(o[1], true);
    if (subid in self._subscriptions) {
    var uri2 = o[1];
    var val = o[2];

    if (ab._debugpubsub) {
    console.group('WAMP Event');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(uri2);
    console.log(val);
    console.groupEnd();
    }

    self._subscriptions[subid].forEach(function (callback) {
    callback(uri2, val);
    });
    } else {
    // ignore unsolicited event!
    }
    } else if (o[0] === ab._MESSAGE_TYPEID_WELCOME) {
    if (self._session_id === null) {
    self._session_id = o[1];
    self._wamp_version = o[2];
    self._server = o[3];

    if (ab._debugrpc || ab._debugpubsub) {
    console.group('WAMP Welcome');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(self._wamp_version);
    console.log(self._server);
    console.groupEnd();
    }

    // only now that we have received the initial server-to-client
    // welcome message, fire application onopen() hook
    if (self._websocket_onopen !== null) {
    self._websocket_onopen();
    }
    } else {
    throw 'protocol error (welcome message received more than once)';
    }
    }
    };

    self._websocket.onopen = function (e) {
    // check if we can speak WAMP!
    if (self._websocket.protocol !== ab._subprotocol) {
    if (typeof self._websocket.protocol === 'undefined') {
    // i.e. Safari does subprotocol negotiation (broken), but then
    // does NOT set the protocol attribute of the websocket object (broken)
    //
    if (ab._debugws) {
    console.group('WS Warning');
    console.info(self._wsuri);
    console.log('WebSocket object has no protocol attribute: WAMP subprotocol check skipped!');
    console.groupEnd();
    }
    } else if (self._options && self._options.skipSubprotocolCheck) {
    // WAMP subprotocol check disabled by session option
    //
    if (ab._debugws) {
    console.group('WS Warning');
    console.info(self._wsuri);
    console.log('Server does not speak WAMP, but subprotocol check disabled by option!');
    console.log(self._websocket.protocol);
    console.groupEnd();
    }
    } else {
    // we only speak WAMP .. if the server denied us this, we bail out.
    //
    self._websocket.close(1000, 'server does not speak WAMP');
    throw "server does not speak WAMP (but '" + self._websocket.protocol + "' !)";
    }
    }
    if (ab._debugws) {
    console.group('WAMP Connect');
    console.info(self._wsuri);
    console.log(self._websocket.protocol);
    console.groupEnd();
    }
    self._websocket_connected = true;
    };

    self._websocket.onerror = function (e) {
    // FF fires this upon unclean closes
    // Chrome does not fire this
    };

    self._websocket.onclose = function (e) {
    if (ab._debugws) {
    if (self._websocket_connected) {
    console.log(
    'Autobahn connection to ' +
    self._wsuri +
    ' lost (code ' +
    e.code +
    ", reason '" +
    e.reason +
    "', wasClean " +
    e.wasClean +
    ').',
    );
    } else {
    console.log(
    'Autobahn could not connect to ' +
    self._wsuri +
    ' (code ' +
    e.code +
    ", reason '" +
    e.reason +
    "', wasClean " +
    e.wasClean +
    ').',
    );
    }
    }

    // fire app callback
    if (self._websocket_onclose !== undefined) {
    if (self._websocket_connected) {
    if (e.wasClean) {
    // connection was closed cleanly (closing HS was performed)
    self._websocket_onclose(ab.CONNECTION_CLOSED, 'WS-' + e.code + ': ' + e.reason);
    } else {
    // connection was closed uncleanly (lost without closing HS)
    self._websocket_onclose(ab.CONNECTION_LOST);
    }
    } else {
    // connection could not be established in the first place
    self._websocket_onclose(ab.CONNECTION_UNREACHABLE);
    }
    }

    // cleanup - reconnect requires a new session object!
    self._websocket_connected = false;
    self._wsuri = null;
    self._websocket_onopen = null;
    self._websocket_onclose = null;
    self._websocket = null;
    };

    self.log = function () {
    if (self._options && 'sessionIdent' in self._options) {
    console.group("WAMP Session '" + self._options.sessionIdent + "' [" + self._session_id + ']');
    } else {
    console.group('WAMP Session ' + '[' + self._session_id + ']');
    }
    for (var i = 0; i < arguments.length; ++i) {
    console.log(arguments[i]);
    }
    console.groupEnd();
    };
    };

    ab.Session.prototype._send = function (msg) {
    var self = this;

    if (!self._websocket_connected) {
    throw 'Autobahn not connected';
    }

    var rmsg;
    switch (true) {
    // In the event that prototype library is in existance run the toJSON method prototype provides
    // else run the standard JSON.stringify
    // this is a very clever problem that causes json to be double-quote-encoded.
    case root.Prototype && typeof top.root.__prototype_deleted === 'undefined':
    case typeof msg.toJSON === 'function':
    rmsg = msg.toJSON();
    break;

    // we could do instead
    // msg.toJSON = function(){return msg};
    // rmsg = JSON.stringify(msg);
    default:
    rmsg = JSON.stringify(msg);
    }

    self._websocket.send(rmsg);
    self._txcnt += 1;

    if (ab._debugws) {
    console.group('WS Send');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(self._txcnt);
    console.log(rmsg);
    console.groupEnd();
    }
    };

    ab.Session.prototype.close = function () {
    var self = this;

    if (self._websocket_connected) {
    self._websocket.close();
    } else {
    //throw "Autobahn not connected";
    }
    };

    ab.Session.prototype.sessionid = function () {
    var self = this;
    return self._session_id;
    };

    ab.Session.prototype.wsuri = function () {
    var self = this;
    return self._wsuri;
    };

    ab.Session.prototype.shrink = function (uri, pass) {
    var self = this;
    if (pass === undefined) pass = true;
    return self._prefixes.shrink(uri, pass);
    };

    ab.Session.prototype.resolve = function (curie, pass) {
    var self = this;
    if (pass === undefined) pass = true;
    return self._prefixes.resolve(curie, pass);
    };

    ab.Session.prototype.prefix = function (prefix, uri) {
    var self = this;

    /*
    if (self._prefixes.get(prefix) !== undefined) {
    throw "prefix '" + prefix + "' already defined";
    }
    */

    self._prefixes.set(prefix, uri);

    if (ab._debugrpc || ab._debugpubsub) {
    console.group('WAMP Prefix');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(prefix);
    console.log(uri);
    console.groupEnd();
    }

    var msg = [ab._MESSAGE_TYPEID_PREFIX, prefix, uri];
    self._send(msg);
    };

    ab.Session.prototype.call = function () {
    var self = this;

    var d = new ab.Deferred();
    var callid;
    while (true) {
    callid = ab._newidFast();
    if (!(callid in self._calls)) {
    break;
    }
    }
    self._calls[callid] = d;

    var procuri = self._prefixes.shrink(arguments[0], true);
    var obj = [ab._MESSAGE_TYPEID_CALL, callid, procuri];
    for (var i = 1; i < arguments.length; i += 1) {
    obj.push(arguments[i]);
    }

    self._send(obj);

    if (ab._debugrpc) {
    d._ab_callobj = obj;
    d._ab_tid = self._wsuri + ' [' + self._session_id + '][' + callid + ']';
    console.time(d._ab_tid);
    console.info();
    }

    if (d.promise.then) {
    // whenjs has the actual user promise in an attribute
    return d.promise;
    } else {
    return d;
    }
    };

    ab.Session.prototype.subscribe = function (topicuri, callback) {
    var self = this;

    // subscribe by sending WAMP message when topic not already subscribed
    //
    var rtopicuri = self._prefixes.resolve(topicuri, true);
    if (!(rtopicuri in self._subscriptions)) {
    if (ab._debugpubsub) {
    console.group('WAMP Subscribe');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(topicuri);
    console.log(callback);
    console.groupEnd();
    }

    var msg = [ab._MESSAGE_TYPEID_SUBSCRIBE, topicuri];
    self._send(msg);

    self._subscriptions[rtopicuri] = [];
    }

    // add callback to event listeners list if not already in list
    //
    var i = self._subscriptions[rtopicuri].indexOf(callback);
    if (i === -1) {
    self._subscriptions[rtopicuri].push(callback);
    } else {
    throw 'callback ' + callback + ' already subscribed for topic ' + rtopicuri;
    }
    };

    ab.Session.prototype.unsubscribe = function (topicuri, callback) {
    var self = this;

    var rtopicuri = self._prefixes.resolve(topicuri, true);
    if (!(rtopicuri in self._subscriptions)) {
    throw 'not subscribed to topic ' + rtopicuri;
    } else {
    var removed;
    if (callback !== undefined) {
    var idx = self._subscriptions[rtopicuri].indexOf(callback);
    if (idx !== -1) {
    removed = callback;
    self._subscriptions[rtopicuri].splice(idx, 1);
    } else {
    throw 'no callback ' + callback + ' subscribed on topic ' + rtopicuri;
    }
    } else {
    removed = self._subscriptions[rtopicuri].slice();
    self._subscriptions[rtopicuri] = [];
    }

    if (self._subscriptions[rtopicuri].length === 0) {
    delete self._subscriptions[rtopicuri];

    if (ab._debugpubsub) {
    console.group('WAMP Unsubscribe');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(topicuri);
    console.log(removed);
    console.groupEnd();
    }

    var msg = [ab._MESSAGE_TYPEID_UNSUBSCRIBE, topicuri];
    self._send(msg);
    }
    }
    };

    ab.Session.prototype.publish = function () {
    var self = this;

    var topicuri = arguments[0];
    var event = arguments[1];

    var excludeMe = null;
    var exclude = null;
    var eligible = null;

    var msg = null;

    if (arguments.length > 3) {
    if (!(arguments[2] instanceof Array)) {
    throw 'invalid argument type(s)';
    }
    if (!(arguments[3] instanceof Array)) {
    throw 'invalid argument type(s)';
    }

    exclude = arguments[2];
    eligible = arguments[3];
    msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event, exclude, eligible];
    } else if (arguments.length > 2) {
    if (typeof arguments[2] === 'boolean') {
    excludeMe = arguments[2];
    msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event, excludeMe];
    } else if (arguments[2] instanceof Array) {
    exclude = arguments[2];
    msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event, exclude];
    } else {
    throw 'invalid argument type(s)';
    }
    } else {
    msg = [ab._MESSAGE_TYPEID_PUBLISH, topicuri, event];
    }

    if (ab._debugpubsub) {
    console.group('WAMP Publish');
    console.info(self._wsuri + ' [' + self._session_id + ']');
    console.log(topicuri);
    console.log(event);

    if (excludeMe !== null) {
    console.log(excludeMe);
    } else {
    if (exclude !== null) {
    console.log(exclude);
    if (eligible !== null) {
    console.log(eligible);
    }
    }
    }
    console.groupEnd();
    }

    self._send(msg);
    };

    // allow both 2-party and 3-party authentication/authorization
    // for 3-party: let C sign, but let both the B and C party authorize

    ab.Session.prototype.authreq = function (appkey, extra) {
    return this.call('http://api.wamp.ws/procedure#authreq', appkey, extra);
    };

    ab.Session.prototype.authsign = function (challenge, secret) {
    if (!secret) {
    secret = '';
    }

    return CryptoJS.HmacSHA256(challenge, secret).toString(CryptoJS.enc.Base64);
    };

    ab.Session.prototype.auth = function (signature) {
    return this.call('http://api.wamp.ws/procedure#auth', signature);
    };

    ab._connect = function (peer) {
    // establish session to WAMP server
    var sess = new ab.Session(
    peer.wsuri,

    // fired when session has been opened
    function () {
    peer.connects += 1;
    peer.retryCount = 0;

    // we are connected .. do awesome stuff!
    peer.onConnect(sess);
    },

    // fired when session has been closed
    function (code, reason) {
    var stop = null;

    switch (code) {
    case ab.CONNECTION_CLOSED:
    // the session was closed by the app
    peer.onHangup(code, 'Connection was closed properly [' + reason + ']');
    break;

    case ab.CONNECTION_UNSUPPORTED:
    // fatal: we miss our WebSocket object!
    peer.onHangup(code, 'Browser does not support WebSocket.');
    break;

    case ab.CONNECTION_UNREACHABLE:
    peer.retryCount += 1;

    if (peer.connects === 0) {
    // the connection could not be established in the first place
    // which likely means invalid server WS URI or such things
    peer.onHangup(code, 'Connection could not be established.');
    } else {
    // the connection was established at least once successfully,
    // but now lost .. sane thing is to try automatic reconnects
    if (peer.retryCount <= peer.options.maxRetries) {
    // notify the app of scheduled reconnect
    stop = peer.onHangup(
    ab.CONNECTION_UNREACHABLE_SCHEDULED_RECONNECT,
    'Connection unreachable - scheduled reconnect to occur in ' +
    peer.options.retryDelay / 1000 +
    ' second(s) - attempt ' +
    peer.retryCount +
    ' of ' +
    peer.options.maxRetries +
    '.',
    {
    delay: peer.options.retryDelay,
    retries: peer.retryCount,
    maxretries: peer.options.maxRetries,
    },
    );

    if (!stop) {
    if (ab._debugconnect) {
    console.log('Connection unreachable - retrying (' + peer.retryCount + ') ..');
    }
    root.setTimeout(function () {
    ab._connect(peer);
    }, peer.options.retryDelay);
    } else {
    if (ab._debugconnect) {
    console.log('Connection unreachable - retrying stopped by app');
    }
    peer.onHangup(ab.CONNECTION_RETRIES_EXCEEDED, 'Number of connection retries exceeded.');
    }
    } else {
    peer.onHangup(ab.CONNECTION_RETRIES_EXCEEDED, 'Number of connection retries exceeded.');
    }
    }
    break;

    case ab.CONNECTION_LOST:
    peer.retryCount += 1;

    if (peer.retryCount <= peer.options.maxRetries) {
    // notify the app of scheduled reconnect
    stop = peer.onHangup(
    ab.CONNECTION_LOST_SCHEDULED_RECONNECT,
    'Connection lost - scheduled ' +
    peer.retryCount +
    'th reconnect to occur in ' +
    peer.options.retryDelay / 1000 +
    ' second(s).',
    {
    delay: peer.options.retryDelay,
    retries: peer.retryCount,
    maxretries: peer.options.maxRetries,
    },
    );

    if (!stop) {
    if (ab._debugconnect) {
    console.log('Connection lost - retrying (' + peer.retryCount + ') ..');
    }
    root.setTimeout(function () {
    ab._connect(peer);
    }, peer.options.retryDelay);
    } else {
    if (ab._debugconnect) {
    console.log('Connection lost - retrying stopped by app');
    }
    peer.onHangup(ab.CONNECTION_RETRIES_EXCEEDED, 'Connection lost.');
    }
    } else {
    peer.onHangup(ab.CONNECTION_RETRIES_EXCEEDED, 'Connection lost.');
    }
    break;

    default:
    throw 'unhandled close code in ab._connect';
    }
    },

    peer.options, // forward options to session class for specific WS/WAMP options
    );
    };

    ab.connect = function (wsuri, onconnect, onhangup, options) {
    var peer = {};
    peer.wsuri = wsuri;

    if (!options) {
    peer.options = {};
    } else {
    peer.options = options;
    }

    if (peer.options.retryDelay === undefined) {
    peer.options.retryDelay = 5000;
    }

    if (peer.options.maxRetries === undefined) {
    peer.options.maxRetries = 10;
    }

    if (peer.options.skipSubprotocolCheck === undefined) {
    peer.options.skipSubprotocolCheck = false;
    }

    if (peer.options.skipSubprotocolAnnounce === undefined) {
    peer.options.skipSubprotocolAnnounce = false;
    }

    if (!onconnect) {
    throw 'onConnect handler required!';
    } else {
    peer.onConnect = onconnect;
    }

    if (!onhangup) {
    peer.onHangup = function (code, reason, detail) {
    if (ab._debugconnect) {
    console.log(code, reason, detail);
    }
    };
    } else {
    peer.onHangup = onhangup;
    }

    peer.connects = 0; // total number of successful connects
    peer.retryCount = 0; // number of retries since last successful connect

    ab._connect(peer);
    };

    ab.launch = function (appConfig, onOpen, onClose) {
    function Rpc(session, uri) {
    return function () {
    var args = [uri];
    for (var j = 0; j < arguments.length; ++j) {
    args.push(arguments[j]);
    }
    //arguments.unshift(uri);
    return ab.Session.prototype.call.apply(session, args);
    };
    }

    function createApi(session, perms) {
    session.api = {};
    for (var i = 0; i < perms.rpc.length; ++i) {
    var uri = perms.rpc[i].uri;

    var _method = uri.split('#')[1];
    var _class = uri.split('#')[0].split('/');
    _class = _class[_class.length - 1];

    if (!(_class in session.api)) {
    session.api[_class] = {};
    }

    session.api[_class][_method] = new Rpc(session, uri);
    }
    }

    ab.connect(
    appConfig.wsuri,

    // connection established handler
    function (session) {
    if (!appConfig.appkey || appConfig.appkey === '') {
    // Authenticate as anonymous ..
    session.authreq().then(function () {
    session.auth().then(function (permissions) {
    //createApi(session, permissions);
    if (onOpen) {
    onOpen(session);
    } else if (ab._debugconnect) {
    session.log('Session opened.');
    }
    }, session.log);
    }, session.log);
    } else {
    // Authenticate as appkey ..
    session.authreq(appConfig.appkey, appConfig.appextra).then(function (challenge) {
    var signature = null;

    if (typeof appConfig.appsecret === 'function') {
    signature = appConfig.appsecret(challenge);
    } else {
    // derive secret if salted WAMP-CRA
    var secret = ab.deriveKey(appConfig.appsecret, JSON.parse(challenge).authextra);

    // direct sign
    signature = session.authsign(challenge, secret);
    }

    session.auth(signature).then(function (permissions) {
    //createApi(session, permissions);
    if (onOpen) {
    onOpen(session);
    } else if (ab._debugconnect) {
    session.log('Session opened.');
    }
    }, session.log);
    }, session.log);
    }
    },

    // connection lost handler
    function (code, reason, detail) {
    if (onClose) {
    onClose(code, reason, detail);
    } else if (ab._debugconnect) {
    ab.log('Session closed.', code, reason, detail);
    }
    },

    // WAMP session config
    appConfig.sessionConfig,
    );
    };

    return ab;
    });