UNPKG

token-sockjs-client

Version:

Client libraries for Node-Token-Sockjs server

420 lines (376 loc) 11.8 kB
;(function(global){ var MAX_DELAY = 5 * 1000, MIN_DELAY = 10, dt = 5; var emitterFunctions = ["on", "addListener", "removeListener", "removeAllListeners"]; var noop = function(){}; var nextDelay = function(last){ return Math.min(last * dt, MAX_DELAY); }; var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var uuid = function(){ var i, out = ""; for(i = 0; i < 10; i++) out += chars.charAt(Math.random() * chars.length | 0); return out; }; Object.size = function(obj){ var size = 0, key; for(key in obj){ if(obj.hasOwnProperty(key)) size++; } return size; }; Object.shallowCopy = function(obj){ var key, copy = {}; for(key in obj){ if(obj.hasOwnProperty(key)) copy[key] = obj[key]; } return copy; }; // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys if (!Object.keys) { Object.keys = (function() { 'use strict'; var hasOwnProperty = Object.prototype.hasOwnProperty, hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), dontEnums = [ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' ], dontEnumsLength = dontEnums.length; return function(obj) { if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { throw new TypeError('Object.keys called on non-object'); } var result = [], prop, i; for (prop in obj) { if (hasOwnProperty.call(obj, prop)) { result.push(prop); } } if (hasDontEnumBug) { for (i = 0; i < dontEnumsLength; i++) { if (hasOwnProperty.call(obj, dontEnums[i])) { result.push(dontEnums[i]); } } } return result; }; }()); } var Monitor = function(socket, emitter){ this._socket = socket; this._inTransit = {}; this._emitter = emitter; }; Monitor.prototype.sendMessage = function(data, callback){ if(typeof data === "string") data = JSON.parse(data); data.uuid = uuid(); if(!this._inTransit[data.rpc]) this._inTransit[data.rpc] = {}; this._inTransit[data.rpc][data.uuid] = callback || noop; this._socket.send(JSON.stringify(data)); }; Monitor.prototype.handleResponse = function(data){ var fn = null; if(data.rpc && data.uuid) fn = this._inTransit[data.rpc][data.uuid]; if(fn && typeof fn === "function"){ if(data.error) fn(data.error); else fn(null, data.resp); delete this._inTransit[data.rpc][data.uuid]; if(Object.size(this._inTransit[data.rpc]) === 0) delete this._inTransit[data.rpc]; }if(data.channel){ this._emitter.emit("message", data.channel, data.message); } }; var rpcResponse = function(error, resp, instance, data){ var res = {}; if(error) res.error = error.message || error; else res.data = resp; var out = { rpc: "_rpc", fid: data.fid, resp: res }; instance._socket.send(JSON.stringify(out)); }; var handleInternal = function(instance, command, data){ if(command === "subscribe"){ instance._channels[data.channel] = true; }else if(command === "unsubscribe"){ delete instance._channels[data.channel]; }else if(command === "rpc"){ var fn = instance._actions; data.command.split(".").forEach(function(s){ fn = fn && fn[s] ? fn[s] : null; }); if(fn && typeof fn === "function"){ fn(data.args, function(error, resp){ rpcResponse(error, resp, instance, data); }); } } }; var attemptReconnect = function(tokenSocket){ tokenSocket._connectTimer = setTimeout(function(instance){ resetConnection(instance, function(error){ if(error){ instance._emitter.emit("reconnect", error); attemptReconnect(instance); }else{ instance._emitter.emit("reconnect"); } }); }, tokenSocket._connectDelay, tokenSocket); tokenSocket._connectDelay = nextDelay(tokenSocket._connectDelay); }; var formEncode = function(obj, prefix) { var prop, out = []; for(prop in obj){ if(!obj.hasOwnProperty(prop) || (typeof obj[prop] === "string" && obj[prop].length < 1)) continue; var key = prefix ? prefix + "[" + prop + "]" : prop, val = obj[prop]; out.push(typeof val == "object" ? formEncode(val, key) : encodeURIComponent(key) + "=" + encodeURIComponent(val)); } return out.join("&"); }; var request = function(options, data, callback){ options = Object.shallowCopy(options); if(options.dataType && options.dataType.toLowerCase() === "jsonp"){ var callbackKey = "token_callback_" + new Date().getTime() + "_" + (Math.round(Math.random() * 1e16)).toString(36); var script = global.document.createElement("script"); global[callbackKey] = function(resp) { global.document.body.removeChild(script); delete global[callbackKey]; if(typeof resp === "string"){ try{ resp = JSON.parse(resp); }catch(e){ return callback(new Error(resp || "Error making jsonp request!")); } } callback(resp && resp.error ? resp.error : null, resp); }; script.onerror = function(e){ global.document.body.removeChild(script); delete global[callbackKey]; callback(new Error("Error making jsonp request!")); return false; }; script.onload = function(e){ return false; }; script.src = options.url + (options.url.indexOf("?") > 0 ? "&" : "?") + "callback=" + callbackKey + "&" + formEncode(data || {}); global.document.body.appendChild(script); }else{ var xhr = new global.XMLHttpRequest(); options.url += (data && options.url.indexOf("?") > 0 ? "&" : "?") + formEncode(data || {}); xhr.open(options.method, options.url, true); xhr.onreadystatechange = function(){ if(xhr.readyState === 4){ var msg = xhr.target ? xhr.target.responseText : xhr.responseText; try{ msg = JSON.parse(msg); }catch(ev){} if(xhr.status >= 200 && xhr.status < 300) callback(null, msg); else callback(msg && msg.error ? new Error(msg.error) : msg instanceof Error ? msg : new Error(msg || "Error making HTTP request")); } }; xhr.setRequestHeader("Accept", "application/json"); xhr.send(); } }; var resetConnection = function(tokenSocket, callback){ request(tokenSocket._opts, tokenSocket._authentication, function(error, resp){ if(error || !resp || !resp.token){ error = error || new Error("No token found!"); return typeof callback === "string" ? tokenSocket._emitter.emit(callback, error) : callback(error); } tokenSocket._token = resp.token; tokenSocket._socket = new global.SockJS(tokenSocket._apiRoute + tokenSocket._socketPrefix, null, tokenSocket._sockjs); tokenSocket._socket.onopen = function(){ tokenSocket._monitor.sendMessage({ rpc: "auth", token: tokenSocket._token }, function(error, resp){ callback = typeof callback === "string" ? tokenSocket._emitter.emit.bind(tokenSocket._emitter, callback) : callback; if(error){ callback(error); }else{ delete tokenSocket._closed; clearInterval(tokenSocket._connectTimer); delete tokenSocket._connectTimer; tokenSocket._connectDelay = MIN_DELAY; replay(tokenSocket); callback(); } }); }; tokenSocket._monitor = new Monitor(tokenSocket._socket, tokenSocket._emitter); tokenSocket._socket.onmessage = function(e){ try{ e.data = JSON.parse(e.data); }catch(ev){ return; } if(e.data.internal) handleInternal(tokenSocket, e.data.command, e.data.data); else tokenSocket._monitor.handleResponse(e.data); }; tokenSocket._socket.onclose = function(){ tokenSocket._closed = true; if(tokenSocket._reconnect) attemptReconnect(tokenSocket); }; }); }; var checkAndUseConnection = function(tokenSocket, callback){ if(tokenSocket._closed) tokenSocket._queue.push({ fn: callback }); else callback(); }; var replay = function(tokenSocket){ for(var channel in tokenSocket._channels) tokenSocket.subscribe(channel); tokenSocket._queue.forEach(function(curr){ if(curr.fn && typeof curr.fn === "function") curr.fn(); }); tokenSocket._queue = []; }; var TokenSocket = function(options, actions){ var self = this; self._emitter = new EventEmitter(); emitterFunctions.forEach(function(fn){ self[fn] = self._emitter[fn]; }); self._closed = true; if(!options) options = {}; if(!self._ready && typeof options.ready === "function") self.ready(options.ready); if(!self._onreconnect && typeof options.onreconnect === "function") self.onreconnect(options.onreconnect); if(!options.host) options.host = global.location.host; self._reconnect = typeof options.reconnect === "undefined" ? true : options.reconnect; self._channels = {}; self._ping = options.ping || false; self._sockjs = options.sockjs || {}; self._apiRoute = options.host.indexOf("http") < 0 ? global.location.protocol + "//" + options.host : options.host; self._socketPrefix = options.socketPrefix || "/sockets"; self._tokenPath = options.tokenPath || "/socket/token"; self._actions = typeof actions === "object" ? actions : {}; self._authentication = options.authentication || {}; self._queue = []; self._connectDelay = MIN_DELAY; self._connectTimer = null; self._opts = { method: "GET", url: self._apiRoute + self._tokenPath, dataType: options.host !== global.location.host ? "jsonp" : "json" }; if(self._ping && self._ping > 0){ self._pingTimer = setInterval(function(){ if(!self._closed) self.rpc("_ping", {}); }, self._ping); } resetConnection(self, "ready"); }; TokenSocket.prototype.ready = function(callback){ this._emitter.on("ready", callback); }; TokenSocket.prototype.onreconnect = function(callback){ this._emitter.on("reconnect", callback); }; TokenSocket.prototype.channels = function(){ return Object.keys(this._channels); }; // @rpc is the controller action TokenSocket.prototype.rpc = function(rpc, data, callback){ var self = this; checkAndUseConnection(self, function(){ self._monitor.sendMessage({ rpc: rpc, req: data }, callback); }); }; TokenSocket.prototype.register = function(actions){ this._actions = actions; }; TokenSocket.prototype.subscribe = function(channel, callback){ var self = this; checkAndUseConnection(self, function(){ self._channels[channel] = true; self._monitor.sendMessage({ rpc: "_subscribe", req: { channel: channel } }, callback); }); }; TokenSocket.prototype.unsubscribe = function(channel, callback){ var self = this; checkAndUseConnection(self, function(){ delete self._channels[channel]; self._monitor.sendMessage({ rpc: "_unsubscribe", req: { channel: channel } }, callback); }); }; TokenSocket.prototype.publish = function(channel, data, callback){ var self = this; checkAndUseConnection(self, function(){ self._monitor.sendMessage({ rpc: "_publish", req: { channel: channel, data: data } }, callback); }); }; TokenSocket.prototype.broadcast = function(data, callback){ var self = this; checkAndUseConnection(self, function(){ self._monitor.sendMessage({ rpc: "_broadcast", req: { data: data } }, callback); }); }; TokenSocket.prototype.onmessage = function(callback){ this._emitter.on("message", callback); }; TokenSocket.prototype.end = function(callback){ this._emitter.removeAllListeners("ready"); this._emitter.removeAllListeners("reconnect"); this._emitter.removeAllListeners("message"); this._closed = true; this._socket.onclose = callback; this._socket.close(); }; global.TokenSocket = TokenSocket; }(typeof window === "undefined" ? {} : window));