scalra
Version:
node.js framework to prototype and scale rapidly
1,425 lines (1,147 loc) • 38 kB
JavaScript
/*
Scalra HTML client-binding
Purpose: to send events and get back updates from Scalra servers using RESTful interface
*/
/**
* SR (Scalra JavaScript binding)
* Copyright(c) 2013-2016 Imonology Inc. <dev@scalra.com>
*/
var version = '0.2.5';
var revision = '2016-08-25';
/*
current SR methods:
init init SR library
setRESTServer
setSocketServer
sendEvent
validateEmail
getParameterByName
getQueryString
getGUID
createID // get a numerical random number between 0 and 10000
publish
subscribe
unsubscribe
history:
2014-06-24 add queryServer() to ask entry servers of WebSocket proxies
2015-06-24 modify queryServer() to seek multiple entries then connect to the one with lowest RTT
*/
var HEADER_PARA = 'P';
var HEADER_UPDATE = 'U';
var HEADER_EVENT = 'E';
// config
var PORT_ENTRY = 8080;
var PORT_HTTP_INC = 0;
var PORT_HTTPS_INC = 0;
// default entry server's IP & port
var DEFAULT_ENTRIES = ['src.scalra.com:8080', 'dev.scalra.com:8080'];
// timeout value in milliseconds
var TIMEOUT_QUERY = 1000;
// helper HTTP_GET
// src: http://stackoverflow.com/questions/247483/http-get-request-in-javascript
/* sync version
function httpGet(theUrl) {
var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", theUrl, false );
xmlHttp.send( null );
return xmlHttp.responseText;
}
*/
var onHttpResponse = function (xmlHttp, onDone) {
return function () {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status == 200) {
if (xmlHttp.responseText == "Not found") {
SR.Error('not found');
if (typeof onDone === 'function')
onDone(undefined);
}
// NOTE: it's possible xmlHttp.responseText is empty
else if (xmlHttp.responseText) {
var info = eval ( "(" + xmlHttp.responseText + ")" );
// passback the jsonData
if (typeof onDone === 'function')
onDone(info);
}
}
// return error for other status
else {
SR.Error('xmlHttp request fail with status: ' + xmlHttp.status);
if (typeof onDone === 'function')
onDone(undefined);
}
}
}
};
// ref: http://stackoverflow.com/questions/1255948/post-data-in-json-format-with-javascript
var httpPost = function (url, data, onDone) {
SR.Log('url to post: ' + url);
// construct an HTTP request
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("POST", url, true);
xmlHttp.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
// set callback when response is returned
xmlHttp.onloadend = onHttpResponse(xmlHttp, onDone);
//console.log('data to send:');
//console.log(data);
// send the collected data as JSON
// TODO: may be slow in Firefox?
xmlHttp.send(Object.keys(data).length > 0 ? JSON.stringify(data) : undefined);
}
// async version
var httpGet = function (url, onDone) {
SR.Log('url to get: ' + url);
var xmlHttp = new XMLHttpRequest();
// set callback when response is returned
// returns 'undefined' if no response or request failed
xmlHttp.onreadystatechange = onHttpResponse(xmlHttp, onDone);
xmlHttp.open("GET", url, true);
xmlHttp.send(null);
}
// helper to convert an object to a key-value pair string
// NOTE: cannot go one-level deep for the object, use the POST method for that
// src: http://stackoverflow.com/questions/6566456/how-to-serialize-a-object-into-a-list-of-parameters
var serialiseObject = function (obj) {
var pairs = [];
for (var prop in obj) {
if (!obj.hasOwnProperty(prop)) {
continue;
}
pairs.push(prop + '=' + obj[prop]);
}
return pairs.join('&');
}
var SR = (typeof module === 'undefined' ? {} : module.exports);
(function (exports, global) {
/**
* SR namespace.
*
* @namespace
*/
var SR = exports;
/**
* version
*
* @api public
*/
SR.version = '0.2.4';
/**
* opertion mode
*
* @api public
*/
/*
// useful info under window.location includes:
direct
host: "src.scalra.com:37074"
hostname: "src.scalra.com"
href: "http://src.scalra.com:37074/web/demo-chat.html"
pathname: "/web/demo-chat.html"
port: "37074"
protocol: "http:"
via Entry
host "src.scalra.com:8080"
hostname "src.scalra.com"
href "http://src.scalra.com...obby/web/demo-chat.html"
origin "http://src.scalra.com:8080"
pathname "/syhu/Scalra/lobby/web/demo-chat.html"
port "8080"
protocol "http:"
*/
//
// logging-related
//
SR.Log = function (msg) {
// by default debug is off, need to be manually enabled
if (SR.debug)
console.log(msg);
}
SR.Warn = function (msg) {
// always show warning now
console.warn(msg);
}
SR.Error = function (msg) {
console.error(msg);
}
// basic info
SR.host = {
name: (typeof connectHost === 'string' ? connectHost : window.location.hostname),
port: parseInt(window.location.port)
};
SR.clone = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
SR.mode = SR.host.name.split('.')[0];
SR.Log('SR mode: ' + SR.mode + ' host:');
SR.Log(SR.host);
/**
* Protocol implemented.
*
* @api public
*/
SR.protocol = 1.1;
/**
* client ID
* @api public
*/
// ID for self
SR.id = 0;
//
// modules
//
//
// local variables
//
var serverDomain = undefined;
var socketDomain = undefined;
// see if it's secured connection
var secured = false;
if (typeof securedConn !== 'undefined' && securedConn === true)
secured = true;
else if (window.location.protocol === "https:")
secured = true;
// cached entry server list (to connect to)
var entryServers = [];
// list of default entries
var defaultEntries = [];
// currently selected entry server
var currentEntry = undefined;
// socket for websocket connection
// to-check, if it's genertic enough for socket.io & sockjs (?)
var socket = undefined;
// list of response handlers
var responseHandlers = {};
// generic response callback for system-defined messages
var onResponse = function (type, para) {
// avoid flooding if SR_PUBLISH is sending streaming data
SR.Log('[' + type + '] received');
switch (type) {
//
// pubsub related
//
// when a new published message arrives
case 'SR_MSG':
// handle server-published messages
case 'SR_PUBLISH':
if (onChannelMessages.hasOwnProperty(para.channel)) {
if (typeof onChannelMessages[para.channel] !== 'function')
SR.Error('channel [' + para.channel + '] handler is not a function');
else
onChannelMessages[para.channel](para.msg, para.channel);
}
else
SR.Error('cannot find channel [' + para.channel + '] to publish');
return true;
// when a list of messages arrive (in array)
case 'SR_MSGLIST':
var msg_list = para.msgs;
if (msg_list && msg_list.length > 0 && onChannelMessages.hasOwnProperty(para.channel)) {
for (var i=0; i < msg_list.length; i++)
onChannelMessages[para.channel](msg_list[i], para.channel);
}
return true;
// redirect to another webpage
case 'SR_REDIRECT':
window.location.href = para.url;
return true;
case "SR_NOTIFY" :
SR.Warn('SR_NOTIFY para: ');
SR.Warn(para);
console.log(onChannelMessages);
if (onChannelMessages.hasOwnProperty('notify'))
onChannelMessages['notify'](para, 'notify');
return true;
//
// login related
//
case "SR_LOGIN_RESPONSE":
case "SR_LOGOUT_RESPONSE":
replyLogin(para);
return true;
case "SR_MESSAGE":
alert('SR_MESSAGE: ' + para.msg);
return true;
case "SR_WARNING":
alert('SR_WARNING: ' + para.msg);
return true;
case "SR_ERROR":
alert('SR_ERROR: ' + para.msg);
return true;
default:
// check if custom handlers exist and can handle it
if (responseHandlers.hasOwnProperty(type)) {
var callbacks = responseHandlers[type];
// extract rid if available
var rid = undefined;
if (para.hasOwnProperty('_rid') === true) {
rid = para['_rid'];
delete para['_rid'];
}
if (rid) {
callbacks[rid](para, type);
// remove callback once done
if (rid !== 'keep') {
delete callbacks[rid];
}
}
// otherwise ALL registered callbacks will be called
else {
if (Object.keys(callbacks).length > 1)
SR.Warn('[' + type + '] no rid in update, dispatching to first of ' + Object.keys(callbacks).length + ' callbacks');
// call the first in callbacks then remove it
// so only one callback is called unless it's registered via the 'keep_callback' flag
for (var key in callbacks) {
callbacks[key](para, type);
if (key !== 'keep') {
delete callbacks[key];
break;
}
}
}
return true;
}
// still un-handled
console.error('onResponse: unrecongized type: ' + type);
return false;
}
}
var socketEvents = ['connecting',
'connect_timeout',
'connect_failed',
'connect_error',
'error',
//'disconnect',
'reconnecting',
'reconnect_failed',
'reconnect_error',
'reconnect'];
// remove a given entry server
var removeEntry = function (entry) {
if (typeof entry === 'number' && entry < entryServers.length) {
entryServers.splice(entry, 1);
return true;
}
else if (typeof entry === 'string') {
for (var i=0; i < entryServers.length; i++) {
if (entryServers[i] === entry) {
entryServers.splice(i, 1);
SR.Log('remove entry: ' + entry + '. entries left: ' + entryServers.length);
return true;
}
}
}
return false;
}
// connect to a new websocket
// server_type: 'socketio' 'sockjs'
var connectSocket = function (server_type, socket_url, connHandler, onDone) {
// handles when connection with server is established
var onConnect = function () {
if (typeof connHandler === 'function')
connHandler('connect');
SR.Log('websocket server connected: ' + socket_url);
if (typeof onDone === 'function')
onDone();
}
// disconnect event
var onDisconnect = function (obj) {
if (typeof connHandler === 'function')
connHandler('disconnect');
SR.Warn('disconnected... obj: ');
SR.Warn(obj);
// attempt to re-connect
if (typeof conn_options === 'object') {
removeEntry(currentEntry);
currentEntry = undefined;
SR.Warn('re-connecting websocket with options');
SR.Warn(conn_options);
SR.setSocketServer(conn_options);
}
}
// configure priority of protocol used
//io.configure(function () {
//io.set("transports", ["xhr-polling", "flashsocket", "json-polling"]);
//});
// connect to server
SR.Log('connecting to server: ' + socket_url);
if (server_type === 'socketio') {
socket = io.connect(
socket_url
/*
{
'reconnection delay': 10000,
'reconnection limit': 4,
'max reconnection attempts': 4
}
*/
);
var generateHandler = function (name) {
return function () {
if (typeof connHandler === 'function')
connHandler(name);
}
}
// Return socket connection status and response
for (var i=0; i < socketEvents.length; i++) {
var event_name = socketEvents[i];
socket.on(event_name, generateHandler(event_name));
}
// connection success
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
// move this outside of 'connect' event handling (to avoid duplicates)
socket.on('SRR', function (data) {
SR.Log('SRR received, type: ' + data[HEADER_UPDATE]);
onResponse(data[HEADER_UPDATE], data[HEADER_PARA]);
});
// attach customized send function
socket.sendJSON = function (obj) {
socket.emit('SR', obj);
}
}
else if (server_type === 'sockjs') {
socket = {};
var url = socket_url + '/sockjs';
SR.Log('sockjs URL to connect: ' + url);
// Create a connection to server
var sock = new SockJS(url);
// Open the connection
sock.onopen = function () {
socket.opened = true;
// send cookie explicitly
SR.Warn('sockjs sending cookie:');
SR.Warn(document.cookie);
sock.send(document.cookie);
onConnect();
}
// On connection close
sock.onclose = onDisconnect;
// On receive message from server
sock.onmessage = function (e) {
// Get the content
var obj = JSON.parse(e.data);
onResponse(obj[HEADER_UPDATE], obj[HEADER_PARA]);
}
// attach customized send function
socket.sendJSON = function (obj) {
sock.send(JSON.stringify(obj));
};
}
else {
SR.Error('unrecongizable server_type: ' + server_type + ' please check if socket.io or sockjs is used');
}
}
// execute a callback, safely
SR.safeCall = function (callback) {
var return_value = undefined;
// first check if callback is indeed a function
if (typeof callback !== 'function')
return return_value;
// call the callback with exception catching
try {
var args = Array.prototype.slice.call(arguments);
return_value = callback.apply(this, args.slice(1));
}
catch (e) {
console.error(e);
}
return return_value;
}
// get round trip time towards a given IP:port pair
SR.getRTT = function (ip_port, onDone) {
var url = 'http://' + ip_port + '/event/PING';
// record current time as time to send out query
var startTime = new Date();
httpGet(url, function (res) {
if (!res) {
SR.Error('getRTT error: ');
onDone(ip_port, -1);
return;
}
var type = res[HEADER_UPDATE];
var para = res[HEADER_PARA];
// check if response is correct
if (type === 'PONG') {
var difference = new Date() - startTime;
onDone(ip_port, difference);
}
});
}
// get a list of registered entry servers (from primary, default entry)
// NOTE: primary entry may return itself in the list as well
SR.queryEntry = function (onDone) {
// entry returned
var results = [];
// build default entry list if not available
if (defaultEntries.length === 0) {
for (var i=0; i < DEFAULT_ENTRIES.length; i++)
defaultEntries.push(DEFAULT_ENTRIES[i]);
defaultEntries.push(SR.host.name + ':' + PORT_ENTRY);
}
var host = defaultEntries[0];
// version 1: just get a list of entries
var req = 'http://' + host + '/event/getEntries';
SR.Log('query for entry list: ' + req);
httpGet(req, function (resObj) {
if (resObj) {
// return directly (should be an object of form {IP: string, port: number})
var type = resObj[HEADER_UPDATE];
var list = resObj[HEADER_PARA];
if (type === 'ENTRY_LIST')
results = list;
else
SR.Error('Incorrect response: ' + type + ', expecting ENTRY_LIST to be returned');
onDone(results);
}
else {
SR.Log('default entry: ' + host + ' cannot be reached, try next one...');
defaultEntries.splice(0, 1);
setTimeout(function () {
SR.queryEntry(onDone);
}, 100);
return;
}
});
/*
// version 2: ping each entry server learned
httpGet(req, function (res) {
if (res === undefined) {
SR.Log('queryEntry error');
return SR.safeCall(onDone, results);
}
// processing once RTT estimate is available
var onRTTResponse = function (ip_port, RTT) {
// skip query that have no results
if (RTT < 0)
return;
SR.Log('RTT: ' + ip_port + ' (' + RTT + ')');
results[ip_port] = RTT;
// check if we've timeout (and if so, we return the current results list)
if (RTT > TIMEOUT_QUERY)
onDone(results);
}
var type = res[HEADER_UPDATE];
var para = res[HEADER_PARA];
SR.Log('entry list:');
SR.Log(para);
// ping each of the entry server & get estimated RTT
for (var i in para) {
SR.getRTT(para[i], onRTTResponse);
}
});
*/
}
// get a socket connection point (IP+port) from a given entry server
var queryConnectionPoint = function (entry, conn_type, fullname, onDone) {
// we query from the given entry server
fullname = fullname.replace(/-/g, '/');
var req = 'http://' + entry + '/' + fullname + '/' + conn_type;
SR.Log('queryConnectionPoint: ' + req);
// will return undefined if failed or no response
// otherwise should be an object of form {IP: string, port: number}
httpGet(req, onDone);
}
// query for an IP / port to connect to lobby server
// 'fullname' should be a string of 'owner/project/name'
// conn_type can be HTTP / HTTPS / WS
// should return {IP: string, port: number} in 'onDone'
SR.queryServer = function (fullname, conn_type, onDone) {
// 1: if there are cached entry server, seek the entry server with lowest RTT
// 2: if not, then query for entry server list, repeat step 1
if (entryServers.length === 0) {
// query default entry for list of entries
SR.queryEntry(function (list) {
var timeout = 100;
// if nothing is found
if (list.length === 0) {
SR.Warn('No Response to get entry server list, try again in 2 seconds...');
timeout = 2000;
}
else {
// if we got something
entryServers = list;
}
setTimeout(function () {
SR.queryServer(fullname, conn_type, onDone);
}, timeout);
});
return;
}
// otherwise contact one entry
// TODO: ping each entry then choose the one with lowest RTT
var index = Math.floor(Math.random() * entryServers.length);
// store entry server to try (so we can remove it later if fail)
currentEntry = entryServers[index];
queryConnectionPoint(currentEntry, conn_type, fullname, function (resObj) {
// check for validity of the connection point
if (resObj && typeof resObj.IP === 'string' && typeof resObj.port === 'number')
return SR.safeCall(onDone, resObj);
removeEntry(currentEntry);
SR.Warn('no ConnectionPoint from this entry, remove it & try again...');
setTimeout(function () {
SR.queryServer(fullname, conn_type, onDone);
}, 100);
});
}
// set up server for RESTful calls
SR.setRESTServer = function (port_or_name, onDone) {
// check correctness
if (typeof onDone !== 'function')
onDone = undefined;
var ip_port = undefined;
var conn_type = (secured ? 'https' : 'http');
// TODO: do not hard code port here
// by port (directly)
if (typeof port_or_name === 'number') {
var ip_port = SR.host.name + ':' + (port_or_name + (secured ? PORT_HTTPS_INC : PORT_HTTP_INC));
serverDomain = conn_type + '://' + ip_port;
}
// by name (via entry server)
else {
var fullname = port_or_name.replace(/-/g, '/');
serverDomain = conn_type + '://' + SR.host.name + ':' + PORT_ENTRY + '/' + fullname;
}
console.log('serverDomain: ' + serverDomain);
SR.safeCall(onDone);
}
// setup socket server
/*
options: {
type: 'string', // 'sockio', 'sockjs'
onEvent: 'function',
onDone: 'function',
hostname: 'string', // Scalra server's hostname
port: 'number', // server's port
name: 'string' // lobby server's name
}
*/
// keep a reference of options when re-connecting
var conn_options = undefined;
var eventStack = [];
SR.setSocketServer = function (options) {
if (typeof options !== 'object') {
SR.Warn('no options specified for setSocketServer');
return;
}
// keep a backup copy when we need to auto-reconnect when server breaks
conn_options = options;
//port_or_name, connHandler, onDone, hostname, server_type
// NOTE: server_type may be 'socketio' or 'sockjs'
var server_type = options.type || 'sockjs';
var connHandler = options.onEvent || function (msg) {SR.Log('connection status: ' + msg)};
var _onDone = options.onDone;
var hostname = options.hostname;
var port = options.port;
var name = options.name;
var onDone = function onDone (args) {
SR.safeCall(_onDone, args);
if (Array.isArray(eventStack) && eventStack.length > 0) {
//console.log("Ejecting eventStack...");
for (var key in eventStack) {
SR.sendEvent(
eventStack[key].type,
eventStack[key].para,
eventStack[key].onDone,
eventStack[key].method,
eventStack[key].keep_callback
);
}
delete eventStack;
}
}
if (port === undefined && name === undefined) {
SR.Warn('at least a server name or a port number need to be supplied');
return;
}
// check if socket.io library exists
if (server_type === 'socketio' && typeof io === 'undefined') {
return connHandler('load Socket.IO failed');
}
if (server_type === 'sockjs' && typeof SockJS === 'undefined') {
return connHandler('load SockJS failed');
}
var ip_port = undefined;
var conn_type = (secured ? 'https' : 'http');
// by port (directly)
if (typeof port === 'number') {
hostname = hostname || SR.host.name;
ip_port = hostname + ':' + (port + (secured ? PORT_HTTPS_INC : PORT_HTTP_INC));
socketDomain = conn_type + '://' + ip_port;
connectSocket(server_type, socketDomain, connHandler, onDone);
}
// by name (via entry server)
else {
var fullname = name.replace(/-/g, '/');
// ask for IP/port to make socket connection
SR.queryServer(fullname, (secured ? 'wss' : 'ws'), function (info) {
SR.Log('queryServer response:');
SR.Log(info);
if (info) {
ip_port = info.IP + ':' + info.port;
SR.Log('ip_port: ' + ip_port);
socketDomain = conn_type + '://' + ip_port;
connectSocket(server_type, socketDomain, connHandler, onDone);
}
else {
// NOTE: some serious error has occurred, we stop here, no further attempts will be made
SR.Error('error: queryServer fail for: ' + fullname);
}
});
}
}
/**
*
*
* @param {String} type name of the event
* @Param {Object} para parameter object to be passed
* @api public
*/
// functions
// onDone is a optional function of format:
// onDone(response_type, para);
// returns whether the send was successful
// keep_callback: 'boolean' (whether onDone won't be erased after processing a response), default to: false
SR.sendEvent = function (type, para, onDone, method, keep_callback) {
// check if para is missing (callback is placed instead)
if (typeof para === 'function') {
SR.Log('SR.sendEvent: parameter missing, use empty {} automatically for type [' + type + '], please check your code');
method = onDone;
onDone = para;
para = {};
}
// default to empty parameters
para = para || {};
// store response handler, if available
var rid = SR.createID();
if (typeof onDone === 'function') {
if (responseHandlers.hasOwnProperty(type) === false)
responseHandlers[type] = {};
// whether this callback will be kept, in such case, only ONE callback is stored
// also, no rid will be sent
if (keep_callback === true) {
rid = 'keep';
}
else {
// store rid as part of request's parameter
para._rid = rid;
}
// store callback, indexed by rid
responseHandlers[type][rid] = onDone;
}
// web-socket specific processing
// NOTE: we'll cache requests if sockets are not yet initialized
//if (socket) {
if (conn_options || (typeof connectType !== 'undefined' && (connectType === 'sockjs' || connectType === 'socketio'))) {
// check if we will send directly or queue the event after connection is established
if (socket.opened) {
var obj = {};
obj[HEADER_EVENT] = type;
obj[HEADER_PARA] = para;
socket.sendJSON(obj);
} else {
eventStack.push({
type: type,
para: para,
onDone: onDone,
method: method,
keep_callback: keep_callback
});
}
return;
}
// HTTP-style event sending
if (!serverDomain) {
SR.Error('no server defined, cannot send event');
return;
}
// remove rid from para
// TODO: find better approach
if (para.hasOwnProperty('_rid')) {
delete para['_rid'];
}
var req = serverDomain + '/event/' + type;
//console.log('[' + method + '] sending request: ' + req);
// send message via HTTP (default to POST)
if (method === 'GET') {
// append parameters
req += (para ? ('?' + serialiseObject(para)) : '');
httpGet(req, function (resObj) {
if (resObj === undefined) {
SR.Log('No Response');
return;
}
var type = resObj[HEADER_UPDATE];
var para = resObj[HEADER_PARA];
onResponse(type, para);
});
}
else {
httpPost(req, para, function (resObj) {
if (typeof resObj === 'undefined' || Object.keys(resObj).length === 0) {
SR.Log('No Response');
return;
}
var type = resObj[HEADER_UPDATE];
var para = resObj[HEADER_PARA];
onResponse(type, para);
});
}
}
//
// API-related
//
var APIlist = [];
// load server-defined API
SR.loadAPI = function (onDone) {
// clear API list
SR.API = {};
// build a specific API
var generateAPI = function (name) {
return function (args, onDone) {
if (typeof args === 'function') {
onDone = args;
args = {};
}
console.log('calling API [' + name + ']...');
// NOTE: by default callbacks are always kept
SR.sendEvent(name, args, function (result) {
if (result.err) {
console.error(result.err);
return SR.safeCall(onDone, result.err);
}
SR.safeCall(onDone, null, result.result);
}, undefined, true);
}
}
SR.sendEvent('SR_API_QUERY', function (result) {
if (result.err) {
console.error(result.err);
return SR.safeCall(onDone, result.err);
}
console.log('API available:' + result.result);
APIlist = result.result;
// load each API
for (var i=0; i < APIlist.length; i++) {
var name = APIlist[i];
SR.API[name] = generateAPI(name);
}
// get direct API calls (need to convert from string to function)
// see: https://stackoverflow.com/questions/2573548/given-a-string-describing-a-javascript-function-convert-it-to-a-javascript-func#
SR.sendEvent('SR_API_QUERY_DIRECT', function (result) {
if (result.err) {
console.error(result.err);
return SR.safeCall(onDone, result.err);
}
console.log('direct API available: ' + Object.keys(result.result));
var direct_APIlist = result.result;
// re-generate each API function from string representation
var valid_API = [];
for (var name in direct_APIlist) {
try {
eval("var func = " + direct_APIlist[name]);
SR.API[name] = func;
valid_API.push(name);
} catch (e) {
console.error(e);
}
}
if (valid_API.length > 0) {
console.log('direct API generated: ' + valid_API);
}
// done when both callback and direct function calls are loaded
SR.safeCall(onDone);
});
});
}
//
// helpers
//
// e-mail validate
// src: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
SR.validateEmail = function (email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
// get URL parameter
// src: http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values
SR.getParameterByName = function (name) {
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? undefined : decodeURSRomponent(results[1].replace(/\+/g, " "));
}
// generate local GUID
SR.getGUID = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
// get a numerical random number between 0 and 10000
SR.createID = function () {
var rand = exports.rand = function() {
var f = (arguments[1]) ? arguments[0] : 0;
var t = (arguments[1]) ? arguments[1] : arguments[0];
return Math.floor((Math.random() * (t - f)) + f);
};
return rand(10000);
}
// get querystring
SR.getQueryString = function () {
// get querystring from current webpage
// ref: http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values
var qs = (function (a) {
if (a == "") return {};
var b = {};
for (var i = 0; i < a.length; ++i)
{
var p=a[i].split('=');
if (p.length != 2)
continue;
b[p[0]] = decodeURSRomponent(p[1].replace(/\+/g, " "));
}
return b;
})(window.location.search.substr(1).split('&'));
return qs;
}
//
// pub/sub functions
//
// callback when subscribed messages are received
var onChannelMessages = {};
// init SR and returns an instance
SR.init = function (options) {
var onDone = function () {
console.log('loading API from server...');
SR.loadAPI(function () {
SR.safeCall(options.onDone);
});
}
if (options.type === 'http') {
SR.setRESTServer(options.port, onDone);
} else {
console.log('ready to connect to socket server...');
SR.setSocketServer({
port: options.port || 0,
type: options.type || 'sockjs', // 'sockio', 'sockjs'
onEvent: options.onEvent,
onDone: onDone,
hostname: options.hostname, // Scalra server's hostname
//name: options.api_key + '-lobby' // lobby server's name, assume server is 'lobby' by default
});
}
return this;
}
// publish a particular message to a channel or to an area
// parameters (if first parameter is an object)
// {
// channel: 'string',
// message: 'string',
// id: 'string',
// area: {x: 'number', y: 'number', r: 'number'},
// layer: 'string'
// }
SR.publish = function (channel, msg) {
// check for object-style parameters (new version)
if (typeof channel === 'object') {
var obj = channel;
var area = undefined;
if (typeof obj.area === 'object') {
area = {
x: obj.area.x,
y: obj.area.y,
r: obj.area.z
}
}
SR.sendEvent('SR_PUB', {ch: obj.channel, msg: obj.message, id: obj.id, area: area, layer: obj.layer});
return;
}
// original version
SR.sendEvent('SR_PUBLISH', {channel: channel, msg: msg});
}
// subscribe a channel
// parameters (if first parameter is an object)
// {
// channel: 'string',
// last: 'number',
// id: 'string',
// area: {x: 'number', y: 'number', r: 'number'},
// layer: 'string'
// }
SR.subscribe = function (channel, last, onMsg) {
console.log('subscribing [' + channel + ']...');
// set default values
if (typeof last === 'function' && typeof onMsg === 'undefined') {
onMsg = last;
last = 0;
}
// check for object-style parameters (new version)
if (typeof channel === 'object') {
var obj = channel;
var area = undefined;
if (typeof obj.area === 'object') {
area = {
x: obj.area.x,
y: obj.area.y,
r: obj.area.z
}
}
SR.sendEvent('SR_SUB', {ch: obj.channel, last: obj.last, id: obj.id, area: area, layer: obj.layer});
return;
}
// original version
onChannelMessages[channel] = onMsg;
if (typeof last !== 'number')
last = 0;
SR.sendEvent('SR_SUBSCRIBE', {channel: channel, para: {last: last}});
}
// unsubscribe from a given channel, or an id (for SPS)
// parameters (if first parameter is an object)
// {
// channel: 'string',
// id: 'string'
// }
SR.unsubscribe = function (channel) {
// check for object-style parameters (new version)
if (typeof channel === 'object') {
var obj = channel;
SR.sendEvent('SR_UNSUB', {ch: obj.channel, id: obj.id});
return;
}
// original version
SR.sendEvent('SR_UNSUBSCRIBE', {channel: channel});
}
// receive server-push notifications
SR.enableNotify = function (onNotify) {
//console.log('SR.enableNotify called');
SR.subscribe('notify', 0, function (data, ch) {
//console.log('server push for [notify]:');
//console.log(data);
SR.safeCall(onNotify, data);
});
}
// parameters (if first parameter is an object)
// {
// id: 'string',
// area: {x: 'number', y: 'number', r: 'number'},
// }
SR.move = function (obj) {
SR.sendEvent('SR_MOVE', {id: obj.id, area: {x: obj.x, y: obj.y, r: obj.r}});
}
//
// login functions
//
// callback to return result of login
var onLoginResponse = undefined;
// helper to respond
var replyLogin = function (res) {
if (typeof onLoginResponse === 'function')
onLoginResponse(res);
}
// login id for this user
SR.login_id = '';
// login
// return {code: err_code, msg: err_msg}
// code: 0 success
// 1 fail
// 2 error
// 3 user data incorrect
//
SR.login = function (type, user_data, onDone) {
// verify login_id exists or generate one
if (SR.login_id === '') {
SR.login_id = user_data.login_id || SR.getParameterByName('login_id') || SR.getGUID();
}
user_data = user_data || {};
if (typeof onDone === 'function')
onLoginResponse = onDone;
var err_msg = '';
var verifyUserData = function (require_email, require_password) {
var account = user_data.account || '';
var email = user_data.email || '';
var password = user_data.password || '';
// check account & password
err_msg = '';
if (account === '')
err_msg += 'missing account\n';
else if ((require_email || email !== '') && SR.validateEmail(email) === false)
err_msg += 'incorrect e-mail format\n';
if (require_password && password == '')
err_msg += 'please enter password\n';
return (err_msg === '');
}
var verifyPassword = function () {
if (user_data.password !== user_data.confirm) {
err_msg += 'passwords do not match';
return false;
}
return true;
}
//console.log('login type: ' + type);
// NOTE: right now we assume certain types of response if logic check fails locally,
// but this makes language binding too specific, remove it in future?
switch (type) {
case 'FB':
SR.sendEvent('SR_LOGIN_FB', {login_id: SR.login_id, data: user_data});
break;
// register new account
case 'register':
if (verifyUserData(true, true) === false)
return replyLogin({code: 3, msg: err_msg});
// TODO: encrypt password
SR.sendEvent('SR_LOGIN_REGISTER', {login_id: SR.login_id, data: user_data});
break;
// login by account & password
case 'account':
if (verifyUserData(false, true) === false)
return replyLogin({code: 3, msg: err_msg});
SR.sendEvent('SR_LOGIN_ACCOUNT', {login_id: SR.login_id, data: user_data});
break;
// login by token
case 'token':
SR.sendEvent('SR_LOGIN_TOKEN', {login_id: SR.login_id, data: user_data});
break;
// guest account
case 'guest':
SR.sendEvent('SR_LOGIN_GUEST', {login_id: SR.login_id, data: user_data});
break;
// forget password
case 'getpass':
if (verifyUserData(true, false) === false)
return replyLogin({code: 3, msg: err_msg});
SR.sendEvent('SR_LOGIN_GETPASS', {email: user_data.email});
break;
// set new password
case 'setpass':
if (verifyPassword() === false)
return replyLogin({code: 3, msg: err_msg});
if (typeof user_data.token !== 'string')
return replyLogin({code: 3, msg: 'no setpass token'});
SR.sendEvent('SR_LOGIN_SETPASS', {'password': user_data.password, 'token': user_data.token});
break;
// get account from login_id
case 'getaccount':
SR.sendEvent('SR_LOGIN_QUERY_ACCOUNT', {});
break;
// logout
case 'logout':
SR.sendEvent('SR_LOGOUT', {'account': user_data.account});
break;
// add local account
case 'addlocal':
SR.sendEvent('SR_ADDLOCAL', user_data);
break;
default:
// send event directly
SR.sendEvent(type, user_data);
break;
//return replyLogin({code: 2, msg: 'login method unknown: ' + type});
}
}
//
// init
//
// ID for self
SR.id = SR.getGUID();
})('object' === typeof module ? module.exports : (this.SR = {}), this);