diff options
author | Alan Kligman <ack@mozilla.com> | 2013-04-16 12:45:03 -0400 |
---|---|---|
committer | Alan Kligman <ack@mozilla.com> | 2013-05-23 15:30:25 -0400 |
commit | 4fb9a415268e59744afa683b84268c56e86cca8b (patch) | |
tree | 47266d0ba2fb0ad537d6d419be813ebd0447cd74 | |
parent | 780fee9f3e1f3d6e461be33bd452ed8ecb1d497c (diff) |
Add support for webrtc-based sockets. Moved both backends behind a settings flag, SOCKET_WEBRTC.
-rw-r--r-- | src/library.js | 579 | ||||
-rw-r--r-- | src/library_browser.js | 29 | ||||
-rw-r--r-- | src/settings.js | 3 | ||||
-rw-r--r-- | src/socket.io.js | 3870 | ||||
-rw-r--r-- | src/wrtcp.js | 855 |
5 files changed, 5210 insertions, 126 deletions
diff --git a/src/library.js b/src/library.js index d897556f..285d1359 100644 --- a/src/library.js +++ b/src/library.js @@ -326,24 +326,25 @@ LibraryManager.library = { #else var chunkSize = 1024*1024; // Chunk size in bytes #endif + if (!hasByteServing) chunkSize = datalength; - + // Function to get a range from the remote URL. var doXHR = (function(from, to) { if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); if (to > datalength-1) throw new Error("only " + datalength + " bytes available! programmer error!"); - + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. var xhr = new XMLHttpRequest(); xhr.open('GET', url, false); if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); - + // Some hints to the browser that we want binary data. if (typeof Uint8Array != 'undefined') xhr.responseType = 'arraybuffer'; if (xhr.overrideMimeType) { xhr.overrideMimeType('text/plain; charset=x-user-defined'); } - + xhr.send(null); if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); if (xhr.response !== undefined) { @@ -368,7 +369,7 @@ LibraryManager.library = { this._chunkSize = chunkSize; this.lengthKnown = true; } - + var lazyArray = new LazyUint8Array(); Object.defineProperty(lazyArray, "length", { get: function() { @@ -4467,7 +4468,7 @@ LibraryManager.library = { } return pdest|0; }, - + strlwr__deps:['tolower'], strlwr: function(pstr){ var i = 0; @@ -4478,7 +4479,7 @@ LibraryManager.library = { i++; } }, - + strupr__deps:['toupper'], strupr: function(pstr){ var i = 0; @@ -4660,7 +4661,7 @@ LibraryManager.library = { if (size < 0) { size = 0; } - + var newStr = _malloc(size + 1); {{{ makeCopyValues('newStr', 'ptr', 'size', 'null', null, 1) }}}; {{{ makeSetValue('newStr', 'size', '0', 'i8') }}}; @@ -7070,9 +7071,11 @@ LibraryManager.library = { ['i32', 'h_length'], ['i8**', 'h_addr_list'], ]), + gethostbyname__deps: ['__hostent_struct_layout'], gethostbyname: function(name) { name = Pointer_stringify(name); + console.log("gethostbyname: ", name); if (!_gethostbyname.id) { _gethostbyname.id = 1; _gethostbyname.table = {}; @@ -7112,17 +7115,24 @@ LibraryManager.library = { // sockets. Note that the implementation assumes all sockets are always // nonblocking // ========================================================================== - - $Sockets__deps: ['__setErrNo', '$ERRNO_CODES'], + $Sockets__deps: ['__setErrNo', '$ERRNO_CODES', + function() { return 'io = ' + read('socket.io.js') + ';\n' }, + function() { return 'Peer = ' + read('wrtcp.js') + ';\n' }], $Sockets: { - BACKEND_WEBSOCKETS: 0, - BACKEND_WEBRTC: 1, BUFFER_SIZE: 10*1024, // initial size MAX_BUFFER_SIZE: 10*1024*1024, // maximum size we will grow the buffer - backend: 0, // default to websockets nextFd: 1, fds: {}, + nextport: 1, + maxport: 65535, + peer: null, + connections: {}, + portmap: {}, + localAddr: 0xfe00000a, + addrPool: [ 0x0200000a, 0x0300000a, 0x0400000a, 0x0500000a, + 0x0600000a, 0x0700000a, 0x0800000a, 0x0900000a, 0x0a00000a, + 0x0b00000a, 0x0c00000a, 0x0d00000a, 0x0e00000a], /* 0x0100000a is reserved */ sockaddr_in_layout: Runtime.generateStructInfo([ ['i32', 'sin_family'], ['i16', 'sin_port'], @@ -7139,114 +7149,365 @@ LibraryManager.library = { ['i32', 'msg_controllen'], ['i32', 'msg_flags'], ]), + }, - backends: { - 0: { // websockets - connect: function(info) { - console.log('opening ws://' + info.host + ':' + info.port); - info.socket = new WebSocket('ws://' + info.host + ':' + info.port, ['binary']); - info.socket.binaryType = 'arraybuffer'; +#if SOCKET_WEBRTC + socket__deps: ['$Sockets'], + socket: function(family, type, protocol) { + var fd = Sockets.nextFd++; + assert(fd < 64); // select() assumes socket fd values are in 0..63 + var stream = type == {{{ cDefine('SOCK_STREAM') }}}; + if (protocol) { + assert(stream == (protocol == {{{ cDefine('IPPROTO_TCP') }}})); // if stream, must be tcp + } - var i32Temp = new Uint32Array(1); - var i8Temp = new Uint8Array(i32Temp.buffer); + // Open the peer connection if we don't have it already + if(null == Sockets.peer) { + var host = Module['host']; + var broker = Module['webrtc']['broker']; + var session = Module['webrtc']['session']; + var peer = new Peer(broker); + var listenOptions = Module['webrtc']['hostOptions'] || {}; + peer.onconnection = function(connection) { + console.log('connected'); + var addr; + // Assign 10.0.0.1 to the host + if(session && session === connection['route']) { + addr = 0x0100000a; // 10.0.0.1 + } else { + addr = Sockets.addrPool.shift(); + } + connection['addr'] = addr; + Sockets.connections[addr] = connection; + connection.ondisconnect = function() { + console.log('disconnect'); + // Don't return the host address (10.0.0.1) to the pool + if(!(session && session === Sockets.connections[addr]['route'])) + Sockets.addrPool.push(addr); + delete Sockets.connections[addr]; + + if(Module['webrtc']['ondisconnect'] && 'function' === typeof Module['webrtc']['ondisconnect']) + Module['webrtc']['ondisconnect'](peer); + }; + connection.onerror = function(error) { + if(Module['webrtc']['onerror'] && 'function' === typeof Module['webrtc']['onerror']) + Module['webrtc']['onerror'](error); + }; + connection.onmessage = function(label, message) { + if('unreliable' === label) + handleMessage(addr, message.data); + } - info.inQueue = []; - info.hasData = function() { return info.inQueue.length > 0 } - if (!info.stream) { - var partialBuffer = null; // in datagram mode, inQueue contains full dgram messages; this buffers incomplete data. Must begin with the beginning of a message - } + if(Module['webrtc']['onconnect'] && 'function' === typeof Module['webrtc']['onconnect']) + Module['webrtc']['onconnect'](peer); + }; + peer.onpending = function(pending) { + console.log('pending from: ', pending['route'], '; initiated by: ', (pending['incoming']) ? 'remote' : 'local'); + }; + peer.onerror = function(error) { + console.error(error); + }; + peer.onroute = function(route) { + if(Module['webrtc']['onpeer'] && 'function' === typeof Module['webrtc']['onpeer']) + Module['webrtc']['onpeer'](peer, route); + }; + function handleMessage(addr, message) { +#if SOCKET_DEBUG + Module.print("received " + message.byteLength + " raw bytes"); +#endif + var header = new Uint16Array(message, 0, 2); + if(Sockets.portmap[header[1]]) { + Sockets.portmap[header[1]].inQueue.push([addr, message]); + } else { + console.log("unable to deliver message: ", addr, header[1], message); + } + } + window.onbeforeunload = function() { + var ids = Object.keys(Sockets.connections); + ids.forEach(function(id) { + Sockets.connections[id].close(); + }); + } + Sockets.peer = peer; + } + + var INCOMING_QUEUE_LENGTH = 64; + + function CircularBuffer(max_length) { + var buffer = new Array(++ max_length); + var head = 0; + var tail = 0; + var length = 0; + + return { + push: function(element) { + buffer[tail ++] = element; + length = Math.min(++ length, max_length - 1); + tail = tail % max_length; + if(tail === head) + head = (head + 1) % max_length; + }, + shift: function(element) { + if(length < 1) return undefined; + + var element = buffer[head]; + -- length; + head = (head + 1) % max_length; + return element; + }, + length: function() { + return length; + } + }; + }; + + Sockets.fds[fd] = { + addr: null, + port: null, + inQueue: new CircularBuffer(INCOMING_QUEUE_LENGTH), + header: new Uint16Array(2), + bound: false + }; + return fd; + }, + + mkport__deps: ['$Sockets'], + mkport: function() { + for(var i = 0; i < Sockets.maxport; ++ i) { + var port = Sockets.nextport ++; + Sockets.nextport = (Sockets.nextport > Sockets.maxport) ? 1 : Sockets.nextport; + if(!Sockets.portmap[port]) { + return port; + } + } + assert(false, 'all available ports are in use!'); + }, + + connect: function() { + + }, + + bind__deps: ['$Sockets', '_inet_ntop_raw', 'ntohs', 'mkport'], + bind: function(fd, addr, addrlen) { + var info = Sockets.fds[fd]; + if (!info) return -1; + if(addr) { + info.port = _ntohs(getValue(addr + Sockets.sockaddr_in_layout.sin_port, 'i16')); + // info.addr = getValue(addr + Sockets.sockaddr_in_layout.sin_addr, 'i32'); + } + if(!info.port) { + info.port = _mkport(); + } + info.addr = Sockets.localAddr; // 10.0.0.254 + info.host = __inet_ntop_raw(info.addr); + info.close = function() { + Sockets.portmap[info.port] = undefined; + } + Sockets.portmap[info.port] = info; + console.log("bind: ", info.host, info.port); + info.bound = true; + }, + + sendmsg__deps: ['$Sockets', 'bind', '_inet_ntop_raw', 'ntohs'], + sendmsg: function(fd, msg, flags) { + var info = Sockets.fds[fd]; + if (!info) return -1; + // if we are not connected, use the address info in the message + if(!info.bound) { + _bind(fd); + } + + var name = {{{ makeGetValue('msg', 'Sockets.msghdr_layout.msg_name', '*') }}}; + assert(name, 'sendmsg on non-connected socket, and no name/address in the message'); + var port = _ntohs(getValue(name + Sockets.sockaddr_in_layout.sin_port, 'i16')); + var addr = getValue(name + Sockets.sockaddr_in_layout.sin_addr, 'i32'); + var connection = Sockets.connections[addr]; + // var host = __inet_ntop_raw(addr); + + if (!(connection && connection.connected)) { + ___setErrNo(ERRNO_CODES.EWOULDBLOCK); + return -1; + } - info.socket.onmessage = function(event) { - assert(typeof event.data !== 'string' && event.data.byteLength); // must get binary data! - var data = new Uint8Array(event.data); // make a typed array view on the array buffer + var iov = {{{ makeGetValue('msg', 'Sockets.msghdr_layout.msg_iov', 'i8*') }}}; + var num = {{{ makeGetValue('msg', 'Sockets.msghdr_layout.msg_iovlen', 'i32') }}}; #if SOCKET_DEBUG - Module.print(['onmessage', data.length, '|', Array.prototype.slice.call(data)]); + Module.print('sendmsg vecs: ' + num); #endif - if (info.stream) { - info.inQueue.push(data); - } else { - // we added headers with message sizes, read those to find discrete messages - if (partialBuffer) { - // append to the partial buffer - var newBuffer = new Uint8Array(partialBuffer.length + data.length); - newBuffer.set(partialBuffer); - newBuffer.set(data, partialBuffer.length); - // forget the partial buffer and work on data - data = newBuffer; - partialBuffer = null; - } - var currPos = 0; - while (currPos+4 < data.length) { - i8Temp.set(data.subarray(currPos, currPos+4)); - var currLen = i32Temp[0]; - assert(currLen > 0); - if (currPos + 4 + currLen > data.length) { - break; // not enough data has arrived - } - currPos += 4; + var totalSize = 0; + for (var i = 0; i < num; i++) { + totalSize += {{{ makeGetValue('iov', '8*i + 4', 'i32') }}}; + } + var data = new Uint8Array(totalSize); + var ret = 0; + for (var i = 0; i < num; i++) { + var currNum = {{{ makeGetValue('iov', '8*i + 4', 'i32') }}}; #if SOCKET_DEBUG - Module.print(['onmessage message', currLen, '|', Array.prototype.slice.call(data.subarray(currPos, currPos+currLen))]); + Module.print('sendmsg curr size: ' + currNum); #endif - info.inQueue.push(data.subarray(currPos, currPos+currLen)); - currPos += currLen; - } - // If data remains, buffer it - if (currPos < data.length) { - partialBuffer = data.subarray(currPos); - } - } - } - function send(data) { - // TODO: if browser accepts views, can optimize this + if (!currNum) continue; + var currBuf = {{{ makeGetValue('iov', '8*i', 'i8*') }}}; + data.set(HEAPU8.subarray(currBuf, currBuf+currNum), ret); + ret += currNum; + } + + info.header[0] = info.port; // src port + info.header[1] = port; // dst port #if SOCKET_DEBUG - Module.print('sender actually sending ' + Array.prototype.slice.call(data)); + Module.print('sendmsg port: ' + info.header[0] + ' -> ' + info.header[1]); + Module.print('sendmsg bytes: ' + data.length + ' | ' + Array.prototype.slice.call(data)); #endif - // ok to use the underlying buffer, we created data and know that the buffer starts at the beginning - info.socket.send(data.buffer); - } - var outQueue = []; - var intervalling = false, interval; - function trySend() { - if (info.socket.readyState != info.socket.OPEN) { - if (!intervalling) { - intervalling = true; - console.log('waiting for socket in order to send'); - interval = setInterval(trySend, 100); - } - return; - } - for (var i = 0; i < outQueue.length; i++) { - send(outQueue[i]); - } - outQueue.length = 0; - if (intervalling) { - intervalling = false; - clearInterval(interval); - } + var buffer = new Uint8Array(info.header.byteLength + data.byteLength); + buffer.set(new Uint8Array(info.header.buffer)); + buffer.set(data, info.header.byteLength); + + connection.send('unreliable', buffer.buffer); + }, + + recvmsg__deps: ['$Sockets', 'bind', '__setErrNo', '$ERRNO_CODES', 'htons'], + recvmsg: function(fd, msg, flags) { + var info = Sockets.fds[fd]; + if (!info) return -1; + // if we are not connected, use the address info in the message + if (!info.port) { + console.log('recvmsg on unbound socket'); + assert(false, 'cannot receive on unbound socket'); + } + if (info.inQueue.length() == 0) { + ___setErrNo(ERRNO_CODES.EWOULDBLOCK); + return -1; + } + + var entry = info.inQueue.shift(); + var addr = entry[0]; + var message = entry[1]; + var header = new Uint16Array(message, 0, info.header.length); + var buffer = new Uint8Array(message, info.header.byteLength); + + var bytes = buffer.length; +#if SOCKET_DEBUG + Module.print('recvmsg port: ' + header[1] + ' <- ' + header[0]); + Module.print('recvmsg bytes: ' + bytes + ' | ' + Array.prototype.slice.call(buffer)); +#endif + // write source + var name = {{{ makeGetValue('msg', 'Sockets.msghdr_layout.msg_name', '*') }}}; + {{{ makeSetValue('name', 'Sockets.sockaddr_in_layout.sin_addr', 'addr', 'i32') }}}; + {{{ makeSetValue('name', 'Sockets.sockaddr_in_layout.sin_port', '_htons(header[0])', 'i16') }}}; + // write data + var ret = bytes; + var iov = {{{ makeGetValue('msg', 'Sockets.msghdr_layout.msg_iov', 'i8*') }}}; + var num = {{{ makeGetValue('msg', 'Sockets.msghdr_layout.msg_iovlen', 'i32') }}}; + var bufferPos = 0; + for (var i = 0; i < num && bytes > 0; i++) { + var currNum = {{{ makeGetValue('iov', '8*i + 4', 'i32') }}}; +#if SOCKET_DEBUG + Module.print('recvmsg loop ' + [i, num, bytes, currNum]); +#endif + if (!currNum) continue; + currNum = Math.min(currNum, bytes); // XXX what should happen when we partially fill a buffer..? + bytes -= currNum; + var currBuf = {{{ makeGetValue('iov', '8*i', 'i8*') }}}; +#if SOCKET_DEBUG + Module.print('recvmsg call recv ' + currNum); +#endif + HEAPU8.set(buffer.subarray(bufferPos, bufferPos + currNum), currBuf); + bufferPos += currNum; + } + return ret; + }, + + shutdown: function(fd, how) { + var info = Sockets.fds[fd]; + if (!info) return -1; + info.close(); + Sockets.fds[fd] = null; + }, + + ioctl: function(fd, request, varargs) { + var info = Sockets.fds[fd]; + if (!info) return -1; + var bytes = 0; + if (info.hasData()) { + bytes = info.inQueue[0].length; + } + var dest = {{{ makeGetValue('varargs', '0', 'i32') }}}; + {{{ makeSetValue('dest', '0', 'bytes', 'i32') }}}; + return 0; + }, + + setsockopt: function(d, level, optname, optval, optlen) { + console.log('ignoring setsockopt command'); + return 0; + }, + + accept: function(fd, addr, addrlen) { + // TODO: webrtc queued incoming connections, etc. + // For now, the model is that bind does a connect, and we "accept" that one connection, + // which has host:port the same as ours. We also return the same socket fd. + var info = Sockets.fds[fd]; + if (!info) return -1; + if (addr) { + setValue(addr + Sockets.sockaddr_in_layout.sin_addr, info.addr, 'i32'); + setValue(addr + Sockets.sockaddr_in_layout.sin_port, info.port, 'i32'); + setValue(addrlen, Sockets.sockaddr_in_layout.__size__, 'i32'); + } + return fd; + }, + + select: function(nfds, readfds, writefds, exceptfds, timeout) { + // readfds are supported, + // writefds checks socket open status + // exceptfds not supported + // timeout is always 0 - fully async + assert(!exceptfds); + + var errorCondition = 0; + + function canRead(info) { + return info.inQueue.length() > 0; + } + + function canWrite(info) { + return true; + } + + function checkfds(nfds, fds, can) { + if (!fds) return 0; + + var bitsSet = 0; + var dstLow = 0; + var dstHigh = 0; + var srcLow = {{{ makeGetValue('fds', 0, 'i32') }}}; + var srcHigh = {{{ makeGetValue('fds', 4, 'i32') }}}; + nfds = Math.min(64, nfds); // fd sets have 64 bits + + for (var fd = 0; fd < nfds; fd++) { + var mask = 1 << (fd % 32), int = fd < 32 ? srcLow : srcHigh; + if (int & mask) { + // index is in the set, check if it is ready for read + var info = Sockets.fds[fd]; + if (info && can(info)) { + // set bit + fd < 32 ? (dstLow = dstLow | mask) : (dstHigh = dstHigh | mask); + bitsSet++; } - info.sender = function(data) { - if (!info.stream) { - // add a header with the message size - var header = new Uint8Array(4); - i32Temp[0] = data.length; - header.set(i8Temp); - outQueue.push(header); - } - outQueue.push(new Uint8Array(data)); - trySend(); - }; } - }, - 1: { // webrtc } + + {{{ makeSetValue('fds', 0, 'dstLow', 'i32') }}}; + {{{ makeSetValue('fds', 4, 'dstHigh', 'i32') }}}; + return bitsSet; } - }, - emscripten_set_network_backend__deps: ['$Sockets'], - emscripten_set_network_backend: function(backend) { - Sockets.backend = backend; + var totalHandles = checkfds(nfds, readfds, canRead) + checkfds(nfds, writefds, canWrite); + if (errorCondition) { + ___setErrNo(ERRNO_CODES.EBADF); + return -1; + } else { + return totalHandles; + } }, - +#else socket__deps: ['$Sockets'], socket: function(family, type, protocol) { var fd = Sockets.nextFd++; @@ -7255,9 +7516,6 @@ LibraryManager.library = { if (protocol) { assert(stream == (protocol == {{{ cDefine('IPPROTO_TCP') }}})); // if stream, must be tcp } - if (Sockets.backend == Sockets.BACKEND_WEBRTC) { - assert(!stream); // If WebRTC, we can only support datagram, not stream - } Sockets.fds[fd] = { connected: false, stream: stream @@ -7282,12 +7540,104 @@ LibraryManager.library = { assert(info.host, 'problem translating fake ip ' + parts); } try { - Sockets.backends[Sockets.backend].connect(info); + console.log('opening ws://' + info.host + ':' + info.port); + info.socket = new WebSocket('ws://' + info.host + ':' + info.port, ['binary']); + info.socket.binaryType = 'arraybuffer'; + + var i32Temp = new Uint32Array(1); + var i8Temp = new Uint8Array(i32Temp.buffer); + + info.inQueue = []; + info.hasData = function() { return info.inQueue.length > 0 } + if (!info.stream) { + var partialBuffer = null; // in datagram mode, inQueue contains full dgram messages; this buffers incomplete data. Must begin with the beginning of a message + } + + info.socket.onmessage = function(event) { + assert(typeof event.data !== 'string' && event.data.byteLength); // must get binary data! + var data = new Uint8Array(event.data); // make a typed array view on the array buffer + #if SOCKET_DEBUG + Module.print(['onmessage', data.length, '|', Array.prototype.slice.call(data)]); + #endif + if (info.stream) { + info.inQueue.push(data); + } else { + // we added headers with message sizes, read those to find discrete messages + if (partialBuffer) { + // append to the partial buffer + var newBuffer = new Uint8Array(partialBuffer.length + data.length); + newBuffer.set(partialBuffer); + newBuffer.set(data, partialBuffer.length); + // forget the partial buffer and work on data + data = newBuffer; + partialBuffer = null; + } + var currPos = 0; + while (currPos+4 < data.length) { + i8Temp.set(data.subarray(currPos, currPos+4)); + var currLen = i32Temp[0]; + assert(currLen > 0); + if (currPos + 4 + currLen > data.length) { + break; // not enough data has arrived + } + currPos += 4; + #if SOCKET_DEBUG + Module.print(['onmessage message', currLen, '|', Array.prototype.slice.call(data.subarray(currPos, currPos+currLen))]); + #endif + info.inQueue.push(data.subarray(currPos, currPos+currLen)); + currPos += currLen; + } + // If data remains, buffer it + if (currPos < data.length) { + partialBuffer = data.subarray(currPos); + } + } + } + function send(data) { + // TODO: if browser accepts views, can optimize this + #if SOCKET_DEBUG + Module.print('sender actually sending ' + Array.prototype.slice.call(data)); + #endif + // ok to use the underlying buffer, we created data and know that the buffer starts at the beginning + info.socket.send(data.buffer); + } + var outQueue = []; + var intervalling = false, interval; + function trySend() { + if (info.socket.readyState != info.socket.OPEN) { + if (!intervalling) { + intervalling = true; + console.log('waiting for socket in order to send'); + interval = setInterval(trySend, 100); + } + return; + } + for (var i = 0; i < outQueue.length; i++) { + send(outQueue[i]); + } + outQueue.length = 0; + if (intervalling) { + intervalling = false; + clearInterval(interval); + } + } + info.sender = function(data) { + if (!info.stream) { + // add a header with the message size + var header = new Uint8Array(4); + i32Temp[0] = data.length; + header.set(i8Temp); + outQueue.push(header); + } + outQueue.push(new Uint8Array(data)); + trySend(); + }; } catch(e) { Module.printErr('Error in connect(): ' + e); ___setErrNo(ERRNO_CODES.EACCES); return -1; } + return 0; }, @@ -7483,12 +7833,12 @@ LibraryManager.library = { // exceptfds not supported // timeout is always 0 - fully async assert(!exceptfds); - + var errorCondition = 0; function canRead(info) { - // make sure hasData exists. - // we do create it when the socket is connected, + // make sure hasData exists. + // we do create it when the socket is connected, // but other implementations may create it lazily if ((info.socket.readyState == WebSocket.CLOSING || info.socket.readyState == WebSocket.CLOSED) && info.inQueue.length == 0) { errorCondition = -1; @@ -7498,8 +7848,8 @@ LibraryManager.library = { } function canWrite(info) { - // make sure socket exists. - // we do create it when the socket is connected, + // make sure socket exists. + // we do create it when the socket is connected, // but other implementations may create it lazily if ((info.socket.readyState == WebSocket.CLOSING || info.socket.readyState == WebSocket.CLOSED)) { errorCondition = -1; @@ -7544,6 +7894,7 @@ LibraryManager.library = { return totalHandles; } }, +#endif socketpair__deps: ['__setErrNo', '$ERRNO_CODES'], socketpair: function(domain, type, protocol, sv) { diff --git a/src/library_browser.js b/src/library_browser.js index 2e6c9150..b81dc852 100644 --- a/src/library_browser.js +++ b/src/library_browser.js @@ -6,7 +6,8 @@ mergeInto(LibraryManager.library, { $Browser__postset: 'Module["requestFullScreen"] = function(lockPointer, resizeCanvas) { Browser.requestFullScreen(lockPointer, resizeCanvas) };\n' + // exports 'Module["requestAnimationFrame"] = function(func) { Browser.requestAnimationFrame(func) };\n' + 'Module["pauseMainLoop"] = function() { Browser.mainLoop.pause() };\n' + - 'Module["resumeMainLoop"] = function() { Browser.mainLoop.resume() };\n', + 'Module["resumeMainLoop"] = function() { Browser.mainLoop.resume() };\n' + + 'Module["getUserMedia"] = function() { Browser.getUserMedia() }', $Browser: { mainLoop: { scheduler: null, @@ -346,7 +347,7 @@ mergeInto(LibraryManager.library, { canvas.requestFullScreen = canvas['requestFullScreen'] || canvas['mozRequestFullScreen'] || (canvas['webkitRequestFullScreen'] ? function() { canvas['webkitRequestFullScreen'](Element['ALLOW_KEYBOARD_INPUT']) } : null); - canvas.requestFullScreen(); + canvas.requestFullScreen(); }, requestAnimationFrame: function(func) { @@ -383,6 +384,12 @@ mergeInto(LibraryManager.library, { setInterval(function() { if (!ABORT) func(); }, timeout); + getUserMedia: function(func) { + if(!window.getUserMedia) { + window.getUserMedia = navigator['getUserMedia'] || + navigator['mozGetUserMedia']; + } + window.getUserMedia(func); }, getMovementX: function(event) { @@ -499,7 +506,7 @@ mergeInto(LibraryManager.library, { {{{ makeSetValue('SDL.screen+Runtime.QUANTUM_SIZE*0', '0', 'flags', 'i32') }}} Browser.updateResizeListeners(); }, - + setWindowedCanvasSize: function() { var canvas = Module['canvas']; canvas.width = this.windowedWidth; @@ -509,7 +516,7 @@ mergeInto(LibraryManager.library, { {{{ makeSetValue('SDL.screen+Runtime.QUANTUM_SIZE*0', '0', 'flags', 'i32') }}} Browser.updateResizeListeners(); } - + }, emscripten_async_wget: function(url, file, onload, onerror) { @@ -546,11 +553,11 @@ mergeInto(LibraryManager.library, { var _request = Pointer_stringify(request); var _param = Pointer_stringify(param); var index = _file.lastIndexOf('/'); - + var http = new XMLHttpRequest(); http.open(_request, _url, true); http.responseType = 'arraybuffer'; - + // LOAD http.onload = function(e) { if (http.status == 200) { @@ -560,20 +567,20 @@ mergeInto(LibraryManager.library, { if (onerror) Runtime.dynCall('vii', onerror, [arg, http.status]); } }; - + // ERROR http.onerror = function(e) { if (onerror) Runtime.dynCall('vii', onerror, [arg, http.status]); }; - + // PROGRESS http.onprogress = function(e) { var percentComplete = (e.position / e.totalSize)*100; if (onprogress) Runtime.dynCall('vii', onprogress, [arg, percentComplete]); }; - + // Useful because the browser can limit the number of redirection - try { + try { if (http.channel instanceof Ci.nsIHttpChannel) http.channel.redirectionLimit = 0; } catch (ex) { /* whatever */ } @@ -588,7 +595,7 @@ mergeInto(LibraryManager.library, { http.send(null); } }, - + emscripten_async_prepare: function(file, onload, onerror) { var _file = Pointer_stringify(file); var data = FS.analyzePath(_file); diff --git a/src/settings.js b/src/settings.js index d3abb06e..b258ce04 100644 --- a/src/settings.js +++ b/src/settings.js @@ -174,6 +174,7 @@ var LIBRARY_DEBUG = 0; // Print out when we enter a library call (library*.js). // want it back. A simple way to set it in C++ is // emscripten_run_script("Runtime.debug = ...;"); var SOCKET_DEBUG = 0; // Log out socket/network data transfer. +var SOCKET_WEBRTC = 1; // Select socket backend, either webrtc or websockets. var OPENAL_DEBUG = 0; // Print out debugging information from our OpenAL implementation. @@ -193,7 +194,7 @@ var DISABLE_EXCEPTION_CATCHING = 0; // Disables generating code to actually catc // introduce silent failures, which is good). // DISABLE_EXCEPTION_CATCHING = 0 - generate code to actually catch exceptions // DISABLE_EXCEPTION_CATCHING = 1 - disable exception catching at all - // DISABLE_EXCEPTION_CATCHING = 2 - disable exception catching, but enables + // DISABLE_EXCEPTION_CATCHING = 2 - disable exception catching, but enables // catching in whitelist // TODO: Make this also remove cxa_begin_catch etc., optimize relooper // for it, etc. (perhaps do all of this as preprocessing on .ll?) diff --git a/src/socket.io.js b/src/socket.io.js new file mode 100644 index 00000000..3fe6a5a7 --- /dev/null +++ b/src/socket.io.js @@ -0,0 +1,3870 @@ +/*! Socket.IO.js build:0.9.11, development. Copyright(c) 2011 LearnBoost <dev@learnboost.com> MIT Licensed */ + +(function(global) { + +var io = ('undefined' === typeof module ? {} : module.exports); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports) { + + /** + * IO namespace. + * + * @namespace + */ + + var io = exports; + + /** + * Socket.IO version + * + * @api public + */ + + io.version = '0.9.11'; + + /** + * Protocol implemented. + * + * @api public + */ + + io.protocol = 1; + + /** + * Available transports, these will be populated with the available transports + * + * @api public + */ + + io.transports = []; + + /** + * Keep track of jsonp callbacks. + * + * @api private + */ + + io.j = []; + + /** + * Keep track of our io.Sockets + * + * @api private + */ + io.sockets = {}; + + + /** + * Manages connections to hosts. + * + * @param {String} uri + * @Param {Boolean} force creation of new socket (defaults to false) + * @api public + */ + + io.connect = function (host, details) { + var uri = io.util.parseUri(host) + , uuri + , socket; + + if (global && global.location) { + uri.protocol = uri.protocol || global.location.protocol.slice(0, -1); + uri.host = uri.host || (global.document + ? global.document.domain : global.location.hostname); + uri.port = uri.port || global.location.port; + } + + uuri = io.util.uniqueUri(uri); + + var options = { + host: uri.host + , secure: 'https' == uri.protocol + , port: uri.port || ('https' == uri.protocol ? 443 : 80) + , query: uri.query || '' + }; + + io.util.merge(options, details); + + if (options['force new connection'] || !io.sockets[uuri]) { + socket = new io.Socket(options); + } + + if (!options['force new connection'] && socket) { + io.sockets[uuri] = socket; + } + + socket = socket || io.sockets[uuri]; + + // if path is different from '' or / + return socket.of(uri.path.length > 1 ? uri.path : ''); + }; + +})('object' === typeof module ? module.exports : (io = {})); +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports) { + + /** + * Utilities namespace. + * + * @namespace + */ + + var util = exports.util = {}; + + /** + * Parses an URI + * + * @author Steven Levithan <stevenlevithan.com> (MIT license) + * @api public + */ + + var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; + + var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', + 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', + 'anchor']; + + util.parseUri = function (str) { + var m = re.exec(str || '') + , uri = {} + , i = 14; + + while (i--) { + uri[parts[i]] = m[i] || ''; + } + + return uri; + }; + + /** + * Produces a unique url that identifies a Socket.IO connection. + * + * @param {Object} uri + * @api public + */ + + util.uniqueUri = function (uri) { + var protocol = uri.protocol + , host = uri.host + , port = uri.port; + + if ('document' in global) { + host = host || document.domain; + port = port || (protocol == 'https' + && document.location.protocol !== 'https:' ? 443 : document.location.port); + } else { + host = host || 'localhost'; + + if (!port && protocol == 'https') { + port = 443; + } + } + + return (protocol || 'http') + '://' + host + ':' + (port || 80); + }; + + /** + * Mergest 2 query strings in to once unique query string + * + * @param {String} base + * @param {String} addition + * @api public + */ + + util.query = function (base, addition) { + var query = util.chunkQuery(base || '') + , components = []; + + util.merge(query, util.chunkQuery(addition || '')); + for (var part in query) { + if (query.hasOwnProperty(part)) { + components.push(part + '=' + query[part]); + } + } + + return components.length ? '?' + components.join('&') : ''; + }; + + /** + * Transforms a querystring in to an object + * + * @param {String} qs + * @api public + */ + + util.chunkQuery = function (qs) { + var query = {} + , params = qs.split('&') + , i = 0 + , l = params.length + , kv; + + for (; i < l; ++i) { + kv = params[i].split('='); + if (kv[0]) { + query[kv[0]] = kv[1]; + } + } + + return query; + }; + + /** + * Executes the given function when the page is loaded. + * + * io.util.load(function () { console.log('page loaded'); }); + * + * @param {Function} fn + * @api public + */ + + var pageLoaded = false; + + util.load = function (fn) { + if ('document' in global && document.readyState === 'complete' || pageLoaded) { + return fn(); + } + + util.on(global, 'load', fn, false); + }; + + /** + * Adds an event. + * + * @api private + */ + + util.on = function (element, event, fn, capture) { + if (element.attachEvent) { + element.attachEvent('on' + event, fn); + } else if (element.addEventListener) { + element.addEventListener(event, fn, capture); + } + }; + + /** + * Generates the correct `XMLHttpRequest` for regular and cross domain requests. + * + * @param {Boolean} [xdomain] Create a request that can be used cross domain. + * @returns {XMLHttpRequest|false} If we can create a XMLHttpRequest. + * @api private + */ + + util.request = function (xdomain) { + + if (xdomain && 'undefined' != typeof XDomainRequest && !util.ua.hasCORS) { + return new XDomainRequest(); + } + + if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) { + return new XMLHttpRequest(); + } + + if (!xdomain) { + try { + return new window[(['Active'].concat('Object').join('X'))]('Microsoft.XMLHTTP'); + } catch(e) { } + } + + return null; + }; + + /** + * XHR based transport constructor. + * + * @constructor + * @api public + */ + + /** + * Change the internal pageLoaded value. + */ + + if ('undefined' != typeof window) { + util.load(function () { + pageLoaded = true; + }); + } + + /** + * Defers a function to ensure a spinner is not displayed by the browser + * + * @param {Function} fn + * @api public + */ + + util.defer = function (fn) { + if (!util.ua.webkit || 'undefined' != typeof importScripts) { + return fn(); + } + + util.load(function () { + setTimeout(fn, 100); + }); + }; + + /** + * Merges two objects. + * + * @api public + */ + + util.merge = function merge (target, additional, deep, lastseen) { + var seen = lastseen || [] + , depth = typeof deep == 'undefined' ? 2 : deep + , prop; + + for (prop in additional) { + if (additional.hasOwnProperty(prop) && util.indexOf(seen, prop) < 0) { + if (typeof target[prop] !== 'object' || !depth) { + target[prop] = additional[prop]; + seen.push(additional[prop]); + } else { + util.merge(target[prop], additional[prop], depth - 1, seen); + } + } + } + + return target; + }; + + /** + * Merges prototypes from objects + * + * @api public + */ + + util.mixin = function (ctor, ctor2) { + util.merge(ctor.prototype, ctor2.prototype); + }; + + /** + * Shortcut for prototypical and static inheritance. + * + * @api private + */ + + util.inherit = function (ctor, ctor2) { + function f() {}; + f.prototype = ctor2.prototype; + ctor.prototype = new f; + }; + + /** + * Checks if the given object is an Array. + * + * io.util.isArray([]); // true + * io.util.isArray({}); // false + * + * @param Object obj + * @api public + */ + + util.isArray = Array.isArray || function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + + /** + * Intersects values of two arrays into a third + * + * @api public + */ + + util.intersect = function (arr, arr2) { + var ret = [] + , longest = arr.length > arr2.length ? arr : arr2 + , shortest = arr.length > arr2.length ? arr2 : arr; + + for (var i = 0, l = shortest.length; i < l; i++) { + if (~util.indexOf(longest, shortest[i])) + ret.push(shortest[i]); + } + + return ret; + }; + + /** + * Array indexOf compatibility. + * + * @see bit.ly/a5Dxa2 + * @api public + */ + + util.indexOf = function (arr, o, i) { + + for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0; + i < j && arr[i] !== o; i++) {} + + return j <= i ? -1 : i; + }; + + /** + * Converts enumerables to array. + * + * @api public + */ + + util.toArray = function (enu) { + var arr = []; + + for (var i = 0, l = enu.length; i < l; i++) + arr.push(enu[i]); + + return arr; + }; + + /** + * UA / engines detection namespace. + * + * @namespace + */ + + util.ua = {}; + + /** + * Whether the UA supports CORS for XHR. + * + * @api public + */ + + util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { + try { + var a = new XMLHttpRequest(); + } catch (e) { + return false; + } + + return a.withCredentials != undefined; + })(); + + /** + * Detect webkit. + * + * @api public + */ + + util.ua.webkit = 'undefined' != typeof navigator + && /webkit/i.test(navigator.userAgent); + + /** + * Detect iPad/iPhone/iPod. + * + * @api public + */ + + util.ua.iDevice = 'undefined' != typeof navigator + && /iPad|iPhone|iPod/i.test(navigator.userAgent); + +})('undefined' != typeof io ? io : module.exports); +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.EventEmitter = EventEmitter; + + /** + * Event emitter constructor. + * + * @api public. + */ + + function EventEmitter () {}; + + /** + * Adds a listener + * + * @api public + */ + + EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (io.util.isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; + }; + + EventEmitter.prototype.addListener = EventEmitter.prototype.on; + + /** + * Adds a volatile listener. + * + * @api public + */ + + EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; + }; + + /** + * Removes a listener. + * + * @api public + */ + + EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (io.util.isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; + }; + + /** + * Removes all listeners for an event. + * + * @api public + */ + + EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; + }; + + /** + * Gets all listeners for a certain event. + * + * @api publci + */ + + EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!io.util.isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; + }; + + /** + * Emits an event. + * + * @api public + */ + + EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = Array.prototype.slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (io.util.isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +/** + * Based on JSON2 (http://www.JSON.org/js.html). + */ + +(function (exports, nativeJSON) { + "use strict"; + + // use native JSON if it's available + if (nativeJSON && nativeJSON.parse){ + return exports.JSON = { + parse: nativeJSON.parse + , stringify: nativeJSON.stringify + }; + } + + var JSON = exports.JSON = {}; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + function date(d, key) { + return isFinite(d.valueOf()) ? + d.getUTCFullYear() + '-' + + f(d.getUTCMonth() + 1) + '-' + + f(d.getUTCDate()) + 'T' + + f(d.getUTCHours()) + ':' + + f(d.getUTCMinutes()) + ':' + + f(d.getUTCSeconds()) + 'Z' : null; + }; + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value instanceof Date) { + value = date(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + +// If the JSON object does not yet have a parse method, give it one. + + JSON.parse = function (text, reviver) { + // The parse method takes a text and an optional reviver function, and returns + // a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + + // The walk method is used to recursively walk the resulting structure so + // that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + + // Parsing happens in four stages. In the first stage, we replace certain + // Unicode characters with escape sequences. JavaScript handles many characters + // incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + + // In the second stage, we run the text against regular expressions that look + // for non-JSON patterns. We are especially concerned with '()' and 'new' + // because they can cause invocation, and '=' because it can cause mutation. + // But just to be safe, we want to reject all unexpected forms. + + // We split the second stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + + // In the third stage we use the eval function to compile the text into a + // JavaScript structure. The '{' operator is subject to a syntactic ambiguity + // in JavaScript: it can begin a block or an object literal. We wrap the text + // in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + + // In the optional fourth stage, we recursively walk the new structure, passing + // each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + + // If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , typeof JSON !== 'undefined' ? JSON : undefined +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Parser namespace. + * + * @namespace + */ + + var parser = exports.parser = {}; + + /** + * Packet types. + */ + + var packets = parser.packets = [ + 'disconnect' + , 'connect' + , 'heartbeat' + , 'message' + , 'json' + , 'event' + , 'ack' + , 'error' + , 'noop' + ]; + + /** + * Errors reasons. + */ + + var reasons = parser.reasons = [ + 'transport not supported' + , 'client not handshaken' + , 'unauthorized' + ]; + + /** + * Errors advice. + */ + + var advice = parser.advice = [ + 'reconnect' + ]; + + /** + * Shortcuts. + */ + + var JSON = io.JSON + , indexOf = io.util.indexOf; + + /** + * Encodes a packet. + * + * @api private + */ + + parser.encodePacket = function (packet) { + var type = indexOf(packets, packet.type) + , id = packet.id || '' + , endpoint = packet.endpoint || '' + , ack = packet.ack + , data = null; + + switch (packet.type) { + case 'error': + var reason = packet.reason ? indexOf(reasons, packet.reason) : '' + , adv = packet.advice ? indexOf(advice, packet.advice) : ''; + + if (reason !== '' || adv !== '') + data = reason + (adv !== '' ? ('+' + adv) : ''); + + break; + + case 'message': + if (packet.data !== '') + data = packet.data; + break; + + case 'event': + var ev = { name: packet.name }; + + if (packet.args && packet.args.length) { + ev.args = packet.args; + } + + data = JSON.stringify(ev); + break; + + case 'json': + data = JSON.stringify(packet.data); + break; + + case 'connect': + if (packet.qs) + data = packet.qs; + break; + + case 'ack': + data = packet.ackId + + (packet.args && packet.args.length + ? '+' + JSON.stringify(packet.args) : ''); + break; + } + + // construct packet with required fragments + var encoded = [ + type + , id + (ack == 'data' ? '+' : '') + , endpoint + ]; + + // data fragment is optional + if (data !== null && data !== undefined) + encoded.push(data); + + return encoded.join(':'); + }; + + /** + * Encodes multiple messages (payload). + * + * @param {Array} messages + * @api private + */ + + parser.encodePayload = function (packets) { + var decoded = ''; + + if (packets.length == 1) + return packets[0]; + + for (var i = 0, l = packets.length; i < l; i++) { + var packet = packets[i]; + decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]; + } + + return decoded; + }; + + /** + * Decodes a packet + * + * @api private + */ + + var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/; + + parser.decodePacket = function (data) { + var pieces = data.match(regexp); + + if (!pieces) return {}; + + var id = pieces[2] || '' + , data = pieces[5] || '' + , packet = { + type: packets[pieces[1]] + , endpoint: pieces[4] || '' + }; + + // whether we need to acknowledge the packet + if (id) { + packet.id = id; + if (pieces[3]) + packet.ack = 'data'; + else + packet.ack = true; + } + + // handle different packet types + switch (packet.type) { + case 'error': + var pieces = data.split('+'); + packet.reason = reasons[pieces[0]] || ''; + packet.advice = advice[pieces[1]] || ''; + break; + + case 'message': + packet.data = data || ''; + break; + + case 'event': + try { + var opts = JSON.parse(data); + packet.name = opts.name; + packet.args = opts.args; + } catch (e) { } + + packet.args = packet.args || []; + break; + + case 'json': + try { + packet.data = JSON.parse(data); + } catch (e) { } + break; + + case 'connect': + packet.qs = data || ''; + break; + + case 'ack': + var pieces = data.match(/^([0-9]+)(\+)?(.*)/); + if (pieces) { + packet.ackId = pieces[1]; + packet.args = []; + + if (pieces[3]) { + try { + packet.args = pieces[3] ? JSON.parse(pieces[3]) : []; + } catch (e) { } + } + } + break; + + case 'disconnect': + case 'heartbeat': + break; + }; + + return packet; + }; + + /** + * Decodes data payload. Detects multiple messages + * + * @return {Array} messages + * @api public + */ + + parser.decodePayload = function (data) { + // IE doesn't like data[i] for unicode chars, charAt works fine + if (data.charAt(0) == '\ufffd') { + var ret = []; + + for (var i = 1, length = ''; i < data.length; i++) { + if (data.charAt(i) == '\ufffd') { + ret.push(parser.decodePacket(data.substr(i + 1).substr(0, length))); + i += Number(length) + 1; + length = ''; + } else { + length += data.charAt(i); + } + } + + return ret; + } else { + return [parser.decodePacket(data)]; + } + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.Transport = Transport; + + /** + * This is the transport template for all supported transport methods. + * + * @constructor + * @api public + */ + + function Transport (socket, sessid) { + this.socket = socket; + this.sessid = sessid; + }; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(Transport, io.EventEmitter); + + + /** + * Indicates whether heartbeats is enabled for this transport + * + * @api private + */ + + Transport.prototype.heartbeats = function () { + return true; + }; + + /** + * Handles the response from the server. When a new response is received + * it will automatically update the timeout, decode the message and + * forwards the response to the onMessage function for further processing. + * + * @param {String} data Response from the server. + * @api private + */ + + Transport.prototype.onData = function (data) { + this.clearCloseTimeout(); + + // If the connection in currently open (or in a reopening state) reset the close + // timeout since we have just received data. This check is necessary so + // that we don't reset the timeout on an explicitly disconnected connection. + if (this.socket.connected || this.socket.connecting || this.socket.reconnecting) { + this.setCloseTimeout(); + } + + if (data !== '') { + // todo: we should only do decodePayload for xhr transports + var msgs = io.parser.decodePayload(data); + + if (msgs && msgs.length) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.onPacket(msgs[i]); + } + } + } + + return this; + }; + + /** + * Handles packets. + * + * @api private + */ + + Transport.prototype.onPacket = function (packet) { + this.socket.setHeartbeatTimeout(); + + if (packet.type == 'heartbeat') { + return this.onHeartbeat(); + } + + if (packet.type == 'connect' && packet.endpoint == '') { + this.onConnect(); + } + + if (packet.type == 'error' && packet.advice == 'reconnect') { + this.isOpen = false; + } + + this.socket.onPacket(packet); + + return this; + }; + + /** + * Sets close timeout + * + * @api private + */ + + Transport.prototype.setCloseTimeout = function () { + if (!this.closeTimeout) { + var self = this; + + this.closeTimeout = setTimeout(function () { + self.onDisconnect(); + }, this.socket.closeTimeout); + } + }; + + /** + * Called when transport disconnects. + * + * @api private + */ + + Transport.prototype.onDisconnect = function () { + if (this.isOpen) this.close(); + this.clearTimeouts(); + this.socket.onDisconnect(); + return this; + }; + + /** + * Called when transport connects + * + * @api private + */ + + Transport.prototype.onConnect = function () { + this.socket.onConnect(); + return this; + }; + + /** + * Clears close timeout + * + * @api private + */ + + Transport.prototype.clearCloseTimeout = function () { + if (this.closeTimeout) { + clearTimeout(this.closeTimeout); + this.closeTimeout = null; + } + }; + + /** + * Clear timeouts + * + * @api private + */ + + Transport.prototype.clearTimeouts = function () { + this.clearCloseTimeout(); + + if (this.reopenTimeout) { + clearTimeout(this.reopenTimeout); + } + }; + + /** + * Sends a packet + * + * @param {Object} packet object. + * @api private + */ + + Transport.prototype.packet = function (packet) { + this.send(io.parser.encodePacket(packet)); + }; + + /** + * Send the received heartbeat message back to server. So the server + * knows we are still connected. + * + * @param {String} heartbeat Heartbeat response from the server. + * @api private + */ + + Transport.prototype.onHeartbeat = function (heartbeat) { + this.packet({ type: 'heartbeat' }); + }; + + /** + * Called when the transport opens. + * + * @api private + */ + + Transport.prototype.onOpen = function () { + this.isOpen = true; + this.clearCloseTimeout(); + this.socket.onOpen(); + }; + + /** + * Notifies the base when the connection with the Socket.IO server + * has been disconnected. + * + * @api private + */ + + Transport.prototype.onClose = function () { + var self = this; + + /* FIXME: reopen delay causing a infinit loop + this.reopenTimeout = setTimeout(function () { + self.open(); + }, this.socket.options['reopen delay']);*/ + + this.isOpen = false; + this.socket.onClose(); + this.onDisconnect(); + }; + + /** + * Generates a connection url based on the Socket.IO URL Protocol. + * See <https://github.com/learnboost/socket.io-node/> for more details. + * + * @returns {String} Connection url + * @api private + */ + + Transport.prototype.prepareUrl = function () { + var options = this.socket.options; + + return this.scheme() + '://' + + options.host + ':' + options.port + '/' + + options.resource + '/' + io.protocol + + '/' + this.name + '/' + this.sessid; + }; + + /** + * Checks if the transport is ready to start a connection. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + Transport.prototype.ready = function (socket, fn) { + fn.call(this); + }; +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.Socket = Socket; + + /** + * Create a new `Socket.IO client` which can establish a persistent + * connection with a Socket.IO enabled server. + * + * @api public + */ + + function Socket (options) { + this.options = { + port: 80 + , secure: false + , document: 'document' in global ? document : false + , resource: 'socket.io' + , transports: io.transports + , 'connect timeout': 10000 + , 'try multiple transports': true + , 'reconnect': true + , 'reconnection delay': 500 + , 'reconnection limit': Infinity + , 'reopen delay': 3000 + , 'max reconnection attempts': 10 + , 'sync disconnect on unload': false + , 'auto connect': true + , 'flash policy port': 10843 + , 'manualFlush': false + }; + + io.util.merge(this.options, options); + + this.connected = false; + this.open = false; + this.connecting = false; + this.reconnecting = false; + this.namespaces = {}; + this.buffer = []; + this.doBuffer = false; + + if (this.options['sync disconnect on unload'] && + (!this.isXDomain() || io.util.ua.hasCORS)) { + var self = this; + io.util.on(global, 'beforeunload', function () { + self.disconnectSync(); + }, false); + } + + if (this.options['auto connect']) { + this.connect(); + } +}; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(Socket, io.EventEmitter); + + /** + * Returns a namespace listener/emitter for this socket + * + * @api public + */ + + Socket.prototype.of = function (name) { + if (!this.namespaces[name]) { + this.namespaces[name] = new io.SocketNamespace(this, name); + + if (name !== '') { + this.namespaces[name].packet({ type: 'connect' }); + } + } + + return this.namespaces[name]; + }; + + /** + * Emits the given event to the Socket and all namespaces + * + * @api private + */ + + Socket.prototype.publish = function () { + this.emit.apply(this, arguments); + + var nsp; + + for (var i in this.namespaces) { + if (this.namespaces.hasOwnProperty(i)) { + nsp = this.of(i); + nsp.$emit.apply(nsp, arguments); + } + } + }; + + /** + * Performs the handshake + * + * @api private + */ + + function empty () { }; + + Socket.prototype.handshake = function (fn) { + var self = this + , options = this.options; + + function complete (data) { + if (data instanceof Error) { + self.connecting = false; + self.onError(data.message); + } else { + fn.apply(null, data.split(':')); + } + }; + + var url = [ + 'http' + (options.secure ? 's' : '') + ':/' + , options.host + ':' + options.port + , options.resource + , io.protocol + , io.util.query(this.options.query, 't=' + +new Date) + ].join('/'); + + if (this.isXDomain() && !io.util.ua.hasCORS) { + var insertAt = document.getElementsByTagName('script')[0] + , script = document.createElement('script'); + + script.src = url + '&jsonp=' + io.j.length; + insertAt.parentNode.insertBefore(script, insertAt); + + io.j.push(function (data) { + complete(data); + script.parentNode.removeChild(script); + }); + } else { + var xhr = io.util.request(); + + xhr.open('GET', url, true); + if (this.isXDomain()) { + xhr.withCredentials = true; + } + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + xhr.onreadystatechange = empty; + + if (xhr.status == 200) { + complete(xhr.responseText); + } else if (xhr.status == 403) { + self.onError(xhr.responseText); + } else { + self.connecting = false; + !self.reconnecting && self.onError(xhr.responseText); + } + } + }; + xhr.send(null); + } + }; + + /** + * Find an available transport based on the options supplied in the constructor. + * + * @api private + */ + + Socket.prototype.getTransport = function (override) { + var transports = override || this.transports, match; + + for (var i = 0, transport; transport = transports[i]; i++) { + if (io.Transport[transport] + && io.Transport[transport].check(this) + && (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) { + return new io.Transport[transport](this, this.sessionid); + } + } + + return null; + }; + + /** + * Connects to the server. + * + * @param {Function} [fn] Callback. + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.connect = function (fn) { + if (this.connecting) { + return this; + } + + var self = this; + self.connecting = true; + + this.handshake(function (sid, heartbeat, close, transports) { + self.sessionid = sid; + self.closeTimeout = close * 1000; + self.heartbeatTimeout = heartbeat * 1000; + if(!self.transports) + self.transports = self.origTransports = (transports ? io.util.intersect( + transports.split(',') + , self.options.transports + ) : self.options.transports); + + self.setHeartbeatTimeout(); + + function connect (transports){ + if (self.transport) self.transport.clearTimeouts(); + + self.transport = self.getTransport(transports); + if (!self.transport) return self.publish('connect_failed'); + + // once the transport is ready + self.transport.ready(self, function () { + self.connecting = true; + self.publish('connecting', self.transport.name); + self.transport.open(); + + if (self.options['connect timeout']) { + self.connectTimeoutTimer = setTimeout(function () { + if (!self.connected) { + self.connecting = false; + + if (self.options['try multiple transports']) { + var remaining = self.transports; + + while (remaining.length > 0 && remaining.splice(0,1)[0] != + self.transport.name) {} + + if (remaining.length){ + connect(remaining); + } else { + self.publish('connect_failed'); + } + } + } + }, self.options['connect timeout']); + } + }); + } + + connect(self.transports); + + self.once('connect', function (){ + clearTimeout(self.connectTimeoutTimer); + + fn && typeof fn == 'function' && fn(); + }); + }); + + return this; + }; + + /** + * Clears and sets a new heartbeat timeout using the value given by the + * server during the handshake. + * + * @api private + */ + + Socket.prototype.setHeartbeatTimeout = function () { + clearTimeout(this.heartbeatTimeoutTimer); + if(this.transport && !this.transport.heartbeats()) return; + + var self = this; + this.heartbeatTimeoutTimer = setTimeout(function () { + self.transport.onClose(); + }, this.heartbeatTimeout); + }; + + /** + * Sends a message. + * + * @param {Object} data packet. + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.packet = function (data) { + if (this.connected && !this.doBuffer) { + this.transport.packet(data); + } else { + this.buffer.push(data); + } + + return this; + }; + + /** + * Sets buffer state + * + * @api private + */ + + Socket.prototype.setBuffer = function (v) { + this.doBuffer = v; + + if (!v && this.connected && this.buffer.length) { + if (!this.options['manualFlush']) { + this.flushBuffer(); + } + } + }; + + /** + * Flushes the buffer data over the wire. + * To be invoked manually when 'manualFlush' is set to true. + * + * @api public + */ + + Socket.prototype.flushBuffer = function() { + this.transport.payload(this.buffer); + this.buffer = []; + }; + + + /** + * Disconnect the established connect. + * + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.disconnect = function () { + if (this.connected || this.connecting) { + if (this.open) { + this.of('').packet({ type: 'disconnect' }); + } + + // handle disconnection immediately + this.onDisconnect('booted'); + } + + return this; + }; + + /** + * Disconnects the socket with a sync XHR. + * + * @api private + */ + + Socket.prototype.disconnectSync = function () { + // ensure disconnection + var xhr = io.util.request(); + var uri = [ + 'http' + (this.options.secure ? 's' : '') + ':/' + , this.options.host + ':' + this.options.port + , this.options.resource + , io.protocol + , '' + , this.sessionid + ].join('/') + '/?disconnect=1'; + + xhr.open('GET', uri, false); + xhr.send(null); + + // handle disconnection immediately + this.onDisconnect('booted'); + }; + + /** + * Check if we need to use cross domain enabled transports. Cross domain would + * be a different port or different domain name. + * + * @returns {Boolean} + * @api private + */ + + Socket.prototype.isXDomain = function () { + + var port = global.location.port || + ('https:' == global.location.protocol ? 443 : 80); + + return this.options.host !== global.location.hostname + || this.options.port != port; + }; + + /** + * Called upon handshake. + * + * @api private + */ + + Socket.prototype.onConnect = function () { + if (!this.connected) { + this.connected = true; + this.connecting = false; + if (!this.doBuffer) { + // make sure to flush the buffer + this.setBuffer(false); + } + this.emit('connect'); + } + }; + + /** + * Called when the transport opens + * + * @api private + */ + + Socket.prototype.onOpen = function () { + this.open = true; + }; + + /** + * Called when the transport closes. + * + * @api private + */ + + Socket.prototype.onClose = function () { + this.open = false; + clearTimeout(this.heartbeatTimeoutTimer); + }; + + /** + * Called when the transport first opens a connection + * + * @param text + */ + + Socket.prototype.onPacket = function (packet) { + this.of(packet.endpoint).onPacket(packet); + }; + + /** + * Handles an error. + * + * @api private + */ + + Socket.prototype.onError = function (err) { + if (err && err.advice) { + if (err.advice === 'reconnect' && (this.connected || this.connecting)) { + this.disconnect(); + if (this.options.reconnect) { + this.reconnect(); + } + } + } + + this.publish('error', err && err.reason ? err.reason : err); + }; + + /** + * Called when the transport disconnects. + * + * @api private + */ + + Socket.prototype.onDisconnect = function (reason) { + var wasConnected = this.connected + , wasConnecting = this.connecting; + + this.connected = false; + this.connecting = false; + this.open = false; + + if (wasConnected || wasConnecting) { + this.transport.close(); + this.transport.clearTimeouts(); + if (wasConnected) { + this.publish('disconnect', reason); + + if ('booted' != reason && this.options.reconnect && !this.reconnecting) { + this.reconnect(); + } + } + } + }; + + /** + * Called upon reconnection. + * + * @api private + */ + + Socket.prototype.reconnect = function () { + this.reconnecting = true; + this.reconnectionAttempts = 0; + this.reconnectionDelay = this.options['reconnection delay']; + + var self = this + , maxAttempts = this.options['max reconnection attempts'] + , tryMultiple = this.options['try multiple transports'] + , limit = this.options['reconnection limit']; + + function reset () { + if (self.connected) { + for (var i in self.namespaces) { + if (self.namespaces.hasOwnProperty(i) && '' !== i) { + self.namespaces[i].packet({ type: 'connect' }); + } + } + self.publish('reconnect', self.transport.name, self.reconnectionAttempts); + } + + clearTimeout(self.reconnectionTimer); + + self.removeListener('connect_failed', maybeReconnect); + self.removeListener('connect', maybeReconnect); + + self.reconnecting = false; + + delete self.reconnectionAttempts; + delete self.reconnectionDelay; + delete self.reconnectionTimer; + delete self.redoTransports; + + self.options['try multiple transports'] = tryMultiple; + }; + + function maybeReconnect () { + if (!self.reconnecting) { + return; + } + + if (self.connected) { + return reset(); + }; + + if (self.connecting && self.reconnecting) { + return self.reconnectionTimer = setTimeout(maybeReconnect, 1000); + } + + if (self.reconnectionAttempts++ >= maxAttempts) { + if (!self.redoTransports) { + self.on('connect_failed', maybeReconnect); + self.options['try multiple transports'] = true; + self.transports = self.origTransports; + self.transport = self.getTransport(); + self.redoTransports = true; + self.connect(); + } else { + self.publish('reconnect_failed'); + reset(); + } + } else { + if (self.reconnectionDelay < limit) { + self.reconnectionDelay *= 2; // exponential back off + } + + self.connect(); + self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts); + self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay); + } + }; + + this.options['try multiple transports'] = false; + this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay); + + this.on('connect', maybeReconnect); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.SocketNamespace = SocketNamespace; + + /** + * Socket namespace constructor. + * + * @constructor + * @api public + */ + + function SocketNamespace (socket, name) { + this.socket = socket; + this.name = name || ''; + this.flags = {}; + this.json = new Flag(this, 'json'); + this.ackPackets = 0; + this.acks = {}; + }; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(SocketNamespace, io.EventEmitter); + + /** + * Copies emit since we override it + * + * @api private + */ + + SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit; + + /** + * Creates a new namespace, by proxying the request to the socket. This + * allows us to use the synax as we do on the server. + * + * @api public + */ + + SocketNamespace.prototype.of = function () { + return this.socket.of.apply(this.socket, arguments); + }; + + /** + * Sends a packet. + * + * @api private + */ + + SocketNamespace.prototype.packet = function (packet) { + packet.endpoint = this.name; + this.socket.packet(packet); + this.flags = {}; + return this; + }; + + /** + * Sends a message + * + * @api public + */ + + SocketNamespace.prototype.send = function (data, fn) { + var packet = { + type: this.flags.json ? 'json' : 'message' + , data: data + }; + + if ('function' == typeof fn) { + packet.id = ++this.ackPackets; + packet.ack = true; + this.acks[packet.id] = fn; + } + + return this.packet(packet); + }; + + /** + * Emits an event + * + * @api public + */ + + SocketNamespace.prototype.emit = function (name) { + var args = Array.prototype.slice.call(arguments, 1) + , lastArg = args[args.length - 1] + , packet = { + type: 'event' + , name: name + }; + + if ('function' == typeof lastArg) { + packet.id = ++this.ackPackets; + packet.ack = 'data'; + this.acks[packet.id] = lastArg; + args = args.slice(0, args.length - 1); + } + + packet.args = args; + + return this.packet(packet); + }; + + /** + * Disconnects the namespace + * + * @api private + */ + + SocketNamespace.prototype.disconnect = function () { + if (this.name === '') { + this.socket.disconnect(); + } else { + this.packet({ type: 'disconnect' }); + this.$emit('disconnect'); + } + + return this; + }; + + /** + * Handles a packet + * + * @api private + */ + + SocketNamespace.prototype.onPacket = function (packet) { + var self = this; + + function ack () { + self.packet({ + type: 'ack' + , args: io.util.toArray(arguments) + , ackId: packet.id + }); + }; + + switch (packet.type) { + case 'connect': + this.$emit('connect'); + break; + + case 'disconnect': + if (this.name === '') { + this.socket.onDisconnect(packet.reason || 'booted'); + } else { + this.$emit('disconnect', packet.reason); + } + break; + + case 'message': + case 'json': + var params = ['message', packet.data]; + + if (packet.ack == 'data') { + params.push(ack); + } else if (packet.ack) { + this.packet({ type: 'ack', ackId: packet.id }); + } + + this.$emit.apply(this, params); + break; + + case 'event': + var params = [packet.name].concat(packet.args); + + if (packet.ack == 'data') + params.push(ack); + + this.$emit.apply(this, params); + break; + + case 'ack': + if (this.acks[packet.ackId]) { + this.acks[packet.ackId].apply(this, packet.args); + delete this.acks[packet.ackId]; + } + break; + + case 'error': + if (packet.advice){ + this.socket.onError(packet); + } else { + if (packet.reason == 'unauthorized') { + this.$emit('connect_failed', packet.reason); + } else { + this.$emit('error', packet.reason); + } + } + break; + } + }; + + /** + * Flag interface. + * + * @api private + */ + + function Flag (nsp, name) { + this.namespace = nsp; + this.name = name; + }; + + /** + * Send a message + * + * @api public + */ + + Flag.prototype.send = function () { + this.namespace.flags[this.name] = true; + this.namespace.send.apply(this.namespace, arguments); + }; + + /** + * Emit an event + * + * @api public + */ + + Flag.prototype.emit = function () { + this.namespace.flags[this.name] = true; + this.namespace.emit.apply(this.namespace, arguments); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.websocket = WS; + + /** + * The WebSocket transport uses the HTML5 WebSocket API to establish an + * persistent connection with the Socket.IO server. This transport will also + * be inherited by the FlashSocket fallback as it provides a API compatible + * polyfill for the WebSockets. + * + * @constructor + * @extends {io.Transport} + * @api public + */ + + function WS (socket) { + io.Transport.apply(this, arguments); + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(WS, io.Transport); + + /** + * Transport name + * + * @api public + */ + + WS.prototype.name = 'websocket'; + + /** + * Initializes a new `WebSocket` connection with the Socket.IO server. We attach + * all the appropriate listeners to handle the responses from the server. + * + * @returns {Transport} + * @api public + */ + + WS.prototype.open = function () { + var query = io.util.query(this.socket.options.query) + , self = this + , Socket + + + if (!Socket) { + Socket = global.MozWebSocket || global.WebSocket; + } + + this.websocket = new Socket(this.prepareUrl() + query); + + this.websocket.onopen = function () { + self.onOpen(); + self.socket.setBuffer(false); + }; + this.websocket.onmessage = function (ev) { + self.onData(ev.data); + }; + this.websocket.onclose = function () { + self.onClose(); + self.socket.setBuffer(true); + }; + this.websocket.onerror = function (e) { + self.onError(e); + }; + + return this; + }; + + /** + * Send a message to the Socket.IO server. The message will automatically be + * encoded in the correct message format. + * + * @returns {Transport} + * @api public + */ + + // Do to a bug in the current IDevices browser, we need to wrap the send in a + // setTimeout, when they resume from sleeping the browser will crash if + // we don't allow the browser time to detect the socket has been closed + if (io.util.ua.iDevice) { + WS.prototype.send = function (data) { + var self = this; + setTimeout(function() { + self.websocket.send(data); + },0); + return this; + }; + } else { + WS.prototype.send = function (data) { + this.websocket.send(data); + return this; + }; + } + + /** + * Payload + * + * @api private + */ + + WS.prototype.payload = function (arr) { + for (var i = 0, l = arr.length; i < l; i++) { + this.packet(arr[i]); + } + return this; + }; + + /** + * Disconnect the established `WebSocket` connection. + * + * @returns {Transport} + * @api public + */ + + WS.prototype.close = function () { + this.websocket.close(); + return this; + }; + + /** + * Handle the errors that `WebSocket` might be giving when we + * are attempting to connect or send messages. + * + * @param {Error} e The error. + * @api private + */ + + WS.prototype.onError = function (e) { + this.socket.onError(e); + }; + + /** + * Returns the appropriate scheme for the URI generation. + * + * @api private + */ + WS.prototype.scheme = function () { + return this.socket.options.secure ? 'wss' : 'ws'; + }; + + /** + * Checks if the browser has support for native `WebSockets` and that + * it's not the polyfill created for the FlashSocket transport. + * + * @return {Boolean} + * @api public + */ + + WS.check = function () { + return ('WebSocket' in global && !('__addTask' in WebSocket)) + || 'MozWebSocket' in global; + }; + + /** + * Check if the `WebSocket` transport support cross domain communications. + * + * @returns {Boolean} + * @api public + */ + + WS.xdomainCheck = function () { + return true; + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('websocket'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.flashsocket = Flashsocket; + + /** + * The FlashSocket transport. This is a API wrapper for the HTML5 WebSocket + * specification. It uses a .swf file to communicate with the server. If you want + * to serve the .swf file from a other server than where the Socket.IO script is + * coming from you need to use the insecure version of the .swf. More information + * about this can be found on the github page. + * + * @constructor + * @extends {io.Transport.websocket} + * @api public + */ + + function Flashsocket () { + io.Transport.websocket.apply(this, arguments); + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(Flashsocket, io.Transport.websocket); + + /** + * Transport name + * + * @api public + */ + + Flashsocket.prototype.name = 'flashsocket'; + + /** + * Disconnect the established `FlashSocket` connection. This is done by adding a + * new task to the FlashSocket. The rest will be handled off by the `WebSocket` + * transport. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.open = function () { + var self = this + , args = arguments; + + WebSocket.__addTask(function () { + io.Transport.websocket.prototype.open.apply(self, args); + }); + return this; + }; + + /** + * Sends a message to the Socket.IO server. This is done by adding a new + * task to the FlashSocket. The rest will be handled off by the `WebSocket` + * transport. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.send = function () { + var self = this, args = arguments; + WebSocket.__addTask(function () { + io.Transport.websocket.prototype.send.apply(self, args); + }); + return this; + }; + + /** + * Disconnects the established `FlashSocket` connection. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.close = function () { + WebSocket.__tasks.length = 0; + io.Transport.websocket.prototype.close.call(this); + return this; + }; + + /** + * The WebSocket fall back needs to append the flash container to the body + * element, so we need to make sure we have access to it. Or defer the call + * until we are sure there is a body element. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + Flashsocket.prototype.ready = function (socket, fn) { + function init () { + var options = socket.options + , port = options['flash policy port'] + , path = [ + 'http' + (options.secure ? 's' : '') + ':/' + , options.host + ':' + options.port + , options.resource + , 'static/flashsocket' + , 'WebSocketMain' + (socket.isXDomain() ? 'Insecure' : '') + '.swf' + ]; + + // Only start downloading the swf file when the checked that this browser + // actually supports it + if (!Flashsocket.loaded) { + if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') { + // Set the correct file based on the XDomain settings + WEB_SOCKET_SWF_LOCATION = path.join('/'); + } + + if (port !== 843) { + WebSocket.loadFlashPolicyFile('xmlsocket://' + options.host + ':' + port); + } + + WebSocket.__initialize(); + Flashsocket.loaded = true; + } + + fn.call(self); + } + + var self = this; + if (document.body) return init(); + + io.util.load(init); + }; + + /** + * Check if the FlashSocket transport is supported as it requires that the Adobe + * Flash Player plug-in version `10.0.0` or greater is installed. And also check if + * the polyfill is correctly loaded. + * + * @returns {Boolean} + * @api public + */ + + Flashsocket.check = function () { + if ( + typeof WebSocket == 'undefined' + || !('__initialize' in WebSocket) || !swfobject + ) return false; + + return swfobject.getFlashPlayerVersion().major >= 10; + }; + + /** + * Check if the FlashSocket transport can be used as cross domain / cross origin + * transport. Because we can't see which type (secure or insecure) of .swf is used + * we will just return true. + * + * @returns {Boolean} + * @api public + */ + + Flashsocket.xdomainCheck = function () { + return true; + }; + + /** + * Disable AUTO_INITIALIZATION + */ + + if (typeof window != 'undefined') { + WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; + } + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('flashsocket'); +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/* SWFObject v2.2 <http://code.google.com/p/swfobject/> + is released under the MIT License <http://www.opensource.org/licenses/mit-license.php> +*/ +if ('undefined' != typeof window) { +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O[(['Active'].concat('Object').join('X'))]!=D){try{var ad=new window[(['Active'].concat('Object').join('X'))](W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y<X;Y++){U[Y]()}}function K(X){if(J){X()}else{U[U.length]=X}}function s(Y){if(typeof O.addEventListener!=D){O.addEventListener("load",Y,false)}else{if(typeof j.addEventListener!=D){j.addEventListener("load",Y,false)}else{if(typeof O.attachEvent!=D){i(O,"onload",Y)}else{if(typeof O.onload=="function"){var X=O.onload;O.onload=function(){X();Y()}}else{O.onload=Y}}}}}function h(){if(T){V()}else{H()}}function V(){var X=j.getElementsByTagName("body")[0];var aa=C(r);aa.setAttribute("type",q);var Z=X.appendChild(aa);if(Z){var Y=0;(function(){if(typeof Z.GetVariable!=D){var ab=Z.GetVariable("$version");if(ab){ab=ab.split(" ")[1].split(",");M.pv=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}else{if(Y<10){Y++;setTimeout(arguments.callee,10);return}}X.removeChild(aa);Z=null;H()})()}else{H()}}function H(){var ag=o.length;if(ag>0){for(var af=0;af<ag;af++){var Y=o[af].id;var ab=o[af].callbackFn;var aa={success:false,id:Y};if(M.pv[0]>0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad<ac;ad++){if(X[ad].getAttribute("name").toLowerCase()!="movie"){ah[X[ad].getAttribute("name")]=X[ad].getAttribute("value")}}P(ai,ah,Y,ab)}else{p(ae);if(ab){ab(aa)}}}}}else{w(Y,true);if(ab){var Z=z(Y);if(Z&&typeof Z.SetVariable!=D){aa.success=true;aa.ref=Z}ab(aa)}}}}}function z(aa){var X=null;var Y=c(aa);if(Y&&Y.nodeName=="OBJECT"){if(typeof Y.SetVariable!=D){X=Y}else{var Z=Y.getElementsByTagName(r)[0];if(Z){X=Z}}}return X}function A(){return !a&&F("6.0.65")&&(M.win||M.mac)&&!(M.wk&&M.wk<312)}function P(aa,ab,X,Z){a=true;E=Z||null;B={success:false,id:X};var ae=c(X);if(ae){if(ae.nodeName=="OBJECT"){l=g(ae);Q=null}else{l=ae;Q=X}aa.id=R;if(typeof aa.width==D||(!/%$/.test(aa.width)&&parseInt(aa.width,10)<310)){aa.width="310"}if(typeof aa.height==D||(!/%$/.test(aa.height)&&parseInt(aa.height,10)<137)){aa.height="137"}j.title=j.title.slice(0,47)+" - Flash Player Installation";var ad=M.ie&&M.win?(['Active'].concat('').join('X')):"PlugIn",ac="MMredirectURL="+O.location.toString().replace(/&/g,"%26")+"&MMplayerType="+ad+"&MMdoctitle="+j.title;if(typeof ab.flashvars!=D){ab.flashvars+="&"+ac}else{ab.flashvars=ac}if(M.ie&&M.win&&ae.readyState!=4){var Y=C("div");X+="SWFObjectNew";Y.setAttribute("id",X);ae.parentNode.insertBefore(Y,ae);ae.style.display="none";(function(){if(ae.readyState==4){ae.parentNode.removeChild(ae)}else{setTimeout(arguments.callee,10)}})()}u(aa,ab,X)}}function p(Y){if(M.ie&&M.win&&Y.readyState!=4){var X=C("div");Y.parentNode.insertBefore(X,Y);X.parentNode.replaceChild(g(Y),X);Y.style.display="none";(function(){if(Y.readyState==4){Y.parentNode.removeChild(Y)}else{setTimeout(arguments.callee,10)}})()}else{Y.parentNode.replaceChild(g(Y),Y)}}function g(ab){var aa=C("div");if(M.win&&M.ie){aa.innerHTML=ab.innerHTML}else{var Y=ab.getElementsByTagName(r)[0];if(Y){var ad=Y.childNodes;if(ad){var X=ad.length;for(var Z=0;Z<X;Z++){if(!(ad[Z].nodeType==1&&ad[Z].nodeName=="PARAM")&&!(ad[Z].nodeType==8)){aa.appendChild(ad[Z].cloneNode(true))}}}}}return aa}function u(ai,ag,Y){var X,aa=c(Y);if(M.wk&&M.wk<312){return X}if(aa){if(typeof ai.id==D){ai.id=Y}if(M.ie&&M.win){var ah="";for(var ae in ai){if(ai[ae]!=Object.prototype[ae]){if(ae.toLowerCase()=="data"){ag.movie=ai[ae]}else{if(ae.toLowerCase()=="styleclass"){ah+=' class="'+ai[ae]+'"'}else{if(ae.toLowerCase()!="classid"){ah+=" "+ae+'="'+ai[ae]+'"'}}}}}var af="";for(var ad in ag){if(ag[ad]!=Object.prototype[ad]){af+='<param name="'+ad+'" value="'+ag[ad]+'" />'}}aa.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+ah+">"+af+"</object>";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab<ac;ab++){I[ab][0].detachEvent(I[ab][1],I[ab][2])}var Z=N.length;for(var aa=0;aa<Z;aa++){y(N[aa])}for(var Y in M){M[Y]=null}M=null;for(var X in swfobject){swfobject[X]=null}swfobject=null})}}();return{registerObject:function(ab,X,aa,Z){if(M.w3&&ab&&X){var Y={};Y.id=ab;Y.swfVersion=X;Y.expressInstall=aa;Y.callbackFn=Z;o[o.length]=Y;w(ab,false)}else{if(Z){Z({success:false,id:ab})}}},getObjectById:function(X){if(M.w3){return z(X)}},embedSWF:function(ab,ah,ae,ag,Y,aa,Z,ad,af,ac){var X={success:false,id:ah};if(M.w3&&!(M.wk&&M.wk<312)&&ab&&ah&&ae&&ag&&Y){w(ah,false);K(function(){ae+="";ag+="";var aj={};if(af&&typeof af===r){for(var al in af){aj[al]=af[al]}}aj.data=ab;aj.width=ae;aj.height=ag;var am={};if(ad&&typeof ad===r){for(var ak in ad){am[ak]=ad[ak]}}if(Z&&typeof Z===r){for(var ai in Z){if(typeof am.flashvars!=D){am.flashvars+="&"+ai+"="+Z[ai]}else{am.flashvars=ai+"="+Z[ai]}}}if(F(Y)){var an=u(aj,am,ah);if(aj.id==ah){w(ah,true)}X.success=true;X.ref=an}else{if(aa&&A()){aj.data=aa;P(aj,am,ah,ac);return}else{w(ah,true)}}if(ac){ac(X)}})}else{if(ac){ac(X)}}},switchOffAutoHideShow:function(){m=false},ua:M,getFlashPlayerVersion:function(){return{major:M.pv[0],minor:M.pv[1],release:M.pv[2]}},hasFlashPlayerVersion:F,createSWF:function(Z,Y,X){if(M.w3){return u(Z,Y,X)}else{return undefined}},showExpressInstall:function(Z,aa,X,Y){if(M.w3&&A()){P(Z,aa,X,Y)}},removeSWF:function(X){if(M.w3){y(X)}},createCSS:function(aa,Z,Y,X){if(M.w3){v(aa,Z,Y,X)}},addDomLoadEvent:K,addLoadEvent:s,getQueryParamValue:function(aa){var Z=j.location.search||j.location.hash;if(Z){if(/\?/.test(Z)){Z=Z.split("?")[1]}if(aa==null){return L(Z)}var Y=Z.split("&");for(var X=0;X<Y.length;X++){if(Y[X].substring(0,Y[X].indexOf("="))==aa){return L(Y[X].substring((Y[X].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(a){var X=c(R);if(X&&l){X.parentNode.replaceChild(l,X);if(Q){w(Q,true);if(M.ie&&M.win){l.style.display="block"}}if(E){E(B)}}a=false}}}}(); +} +// Copyright: Hiroshi Ichikawa <http://gimite.net/en/> +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol + +(function() { + + if ('undefined' == typeof window || window.WebSocket) return; + + var console = window.console; + if (!console || !console.log || !console.error) { + console = {log: function(){ }, error: function(){ }}; + } + + if (!swfobject.hasFlashPlayerVersion("10.0.0")) { + console.error("Flash Player >= 10.0.0 is required."); + return; + } + if (location.protocol == "file:") { + console.error( + "WARNING: web-socket-js doesn't work in file:///... URL " + + "unless you set Flash Security Settings properly. " + + "Open the page via Web server i.e. http://..."); + } + + /** + * This class represents a faux web socket. + * @param {string} url + * @param {array or string} protocols + * @param {string} proxyHost + * @param {int} proxyPort + * @param {string} headers + */ + WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { + var self = this; + self.__id = WebSocket.__nextId++; + WebSocket.__instances[self.__id] = self; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + self.__events = {}; + if (!protocols) { + protocols = []; + } else if (typeof protocols == "string") { + protocols = [protocols]; + } + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + setTimeout(function() { + WebSocket.__addTask(function() { + WebSocket.__flash.create( + self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); + }); + }, 0); + }; + + /** + * Send data to the web socket. + * @param {string} data The data to send to the socket. + * @return {boolean} True for success, false for failure. + */ + WebSocket.prototype.send = function(data) { + if (this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + // We use encodeURIComponent() here, because FABridge doesn't work if + // the argument includes some characters. We don't use escape() here + // because of this: + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions + // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't + // preserve all Unicode characters either e.g. "\uffff" in Firefox. + // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require + // additional testing. + var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount += result; + return false; + } + }; + + /** + * Close this web socket gracefully. + */ + WebSocket.prototype.close = function() { + if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { + return; + } + this.readyState = WebSocket.CLOSING; + WebSocket.__flash.close(this.__id); + }; + + /** + * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) { + this.__events[type] = []; + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) return; + var events = this.__events[type]; + for (var i = events.length - 1; i >= 0; --i) { + if (events[i] === listener) { + events.splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} + * + * @param {Event} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + var events = this.__events[event.type] || []; + for (var i = 0; i < events.length; ++i) { + events[i](event); + } + var handler = this["on" + event.type]; + if (handler) handler(event); + }; + + /** + * Handles an event from Flash. + * @param {Object} flashEvent + */ + WebSocket.prototype.__handleEvent = function(flashEvent) { + if ("readyState" in flashEvent) { + this.readyState = flashEvent.readyState; + } + if ("protocol" in flashEvent) { + this.protocol = flashEvent.protocol; + } + + var jsEvent; + if (flashEvent.type == "open" || flashEvent.type == "error") { + jsEvent = this.__createSimpleEvent(flashEvent.type); + } else if (flashEvent.type == "close") { + // TODO implement jsEvent.wasClean + jsEvent = this.__createSimpleEvent("close"); + } else if (flashEvent.type == "message") { + var data = decodeURIComponent(flashEvent.message); + jsEvent = this.__createMessageEvent("message", data); + } else { + throw "unknown event type: " + flashEvent.type; + } + + this.dispatchEvent(jsEvent); + }; + + WebSocket.prototype.__createSimpleEvent = function(type) { + if (document.createEvent && window.Event) { + var event = document.createEvent("Event"); + event.initEvent(type, false, false); + return event; + } else { + return {type: type, bubbles: false, cancelable: false}; + } + }; + + WebSocket.prototype.__createMessageEvent = function(type, data) { + if (document.createEvent && window.MessageEvent && !window.opera) { + var event = document.createEvent("MessageEvent"); + event.initMessageEvent("message", false, false, data, null, null, window, null); + return event; + } else { + // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. + return {type: type, data: data, bubbles: false, cancelable: false}; + } + }; + + /** + * Define the WebSocket readyState enumeration. + */ + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + WebSocket.__flash = null; + WebSocket.__instances = {}; + WebSocket.__tasks = []; + WebSocket.__nextId = 0; + + /** + * Load a new flash security policy file. + * @param {string} url + */ + WebSocket.loadFlashPolicyFile = function(url){ + WebSocket.__addTask(function() { + WebSocket.__flash.loadManualPolicyFile(url); + }); + }; + + /** + * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. + */ + WebSocket.__initialize = function() { + if (WebSocket.__flash) return; + + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents + // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). + // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash + // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is + // the best we can do as far as we know now. + container.style.position = "absolute"; + if (WebSocket.__isFlashLite()) { + container.style.left = "0px"; + container.style.top = "0px"; + } else { + container.style.left = "-100px"; + container.style.top = "-100px"; + } + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + // See this article for hasPriority: + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, + "webSocketFlash", + "1" /* width */, + "1" /* height */, + "10.0.0" /* SWF version */, + null, + null, + {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, + null, + function(e) { + if (!e.success) { + console.error("[WebSocket] swfobject.embedSWF failed"); + } + }); + }; + + /** + * Called by Flash to notify JS that it's fully loaded and ready + * for communication. + */ + WebSocket.__onFlashInitialized = function() { + // We need to set a timeout here to avoid round-trip calls + // to flash during the initialization process. + setTimeout(function() { + WebSocket.__flash = document.getElementById("webSocketFlash"); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + }, 0); + }; + + /** + * Called by Flash to notify WebSockets events are fired. + */ + WebSocket.__onFlashEvent = function() { + setTimeout(function() { + try { + // Gets events using receiveEvents() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var events = WebSocket.__flash.receiveEvents(); + for (var i = 0; i < events.length; ++i) { + WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); + } + } catch (e) { + console.error(e); + } + }, 0); + return true; + }; + + // Called by Flash. + WebSocket.__log = function(message) { + console.log(decodeURIComponent(message)); + }; + + // Called by Flash. + WebSocket.__error = function(message) { + console.error(decodeURIComponent(message)); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + }; + + /** + * Test if the browser is running flash lite. + * @return {boolean} True if flash lite is running, false otherwise. + */ + WebSocket.__isFlashLite = function() { + if (!window.navigator || !window.navigator.mimeTypes) { + return false; + } + var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; + if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { + return false; + } + return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + if (window.addEventListener) { + window.addEventListener("load", function(){ + WebSocket.__initialize(); + }, false); + } else { + window.attachEvent("onload", function(){ + WebSocket.__initialize(); + }); + } + } + +})(); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + * + * @api public + */ + + exports.XHR = XHR; + + /** + * XHR constructor + * + * @costructor + * @api public + */ + + function XHR (socket) { + if (!socket) return; + + io.Transport.apply(this, arguments); + this.sendBuffer = []; + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(XHR, io.Transport); + + /** + * Establish a connection + * + * @returns {Transport} + * @api public + */ + + XHR.prototype.open = function () { + this.socket.setBuffer(false); + this.onOpen(); + this.get(); + + // we need to make sure the request succeeds since we have no indication + // whether the request opened or not until it succeeded. + this.setCloseTimeout(); + + return this; + }; + + /** + * Check if we need to send data to the Socket.IO server, if we have data in our + * buffer we encode it and forward it to the `post` method. + * + * @api private + */ + + XHR.prototype.payload = function (payload) { + var msgs = []; + + for (var i = 0, l = payload.length; i < l; i++) { + msgs.push(io.parser.encodePacket(payload[i])); + } + + this.send(io.parser.encodePayload(msgs)); + }; + + /** + * Send data to the Socket.IO server. + * + * @param data The message + * @returns {Transport} + * @api public + */ + + XHR.prototype.send = function (data) { + this.post(data); + return this; + }; + + /** + * Posts a encoded message to the Socket.IO server. + * + * @param {String} data A encoded message. + * @api private + */ + + function empty () { }; + + XHR.prototype.post = function (data) { + var self = this; + this.socket.setBuffer(true); + + function stateChange () { + if (this.readyState == 4) { + this.onreadystatechange = empty; + self.posting = false; + + if (this.status == 200){ + self.socket.setBuffer(false); + } else { + self.onClose(); + } + } + } + + function onload () { + this.onload = empty; + self.socket.setBuffer(false); + }; + + this.sendXHR = this.request('POST'); + + if (global.XDomainRequest && this.sendXHR instanceof XDomainRequest) { + this.sendXHR.onload = this.sendXHR.onerror = onload; + } else { + this.sendXHR.onreadystatechange = stateChange; + } + + this.sendXHR.send(data); + }; + + /** + * Disconnects the established `XHR` connection. + * + * @returns {Transport} + * @api public + */ + + XHR.prototype.close = function () { + this.onClose(); + return this; + }; + + /** + * Generates a configured XHR request + * + * @param {String} url The url that needs to be requested. + * @param {String} method The method the request should use. + * @returns {XMLHttpRequest} + * @api private + */ + + XHR.prototype.request = function (method) { + var req = io.util.request(this.socket.isXDomain()) + , query = io.util.query(this.socket.options.query, 't=' + +new Date); + + req.open(method || 'GET', this.prepareUrl() + query, true); + + if (method == 'POST') { + try { + if (req.setRequestHeader) { + req.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); + } else { + // XDomainRequest + req.contentType = 'text/plain'; + } + } catch (e) {} + } + + return req; + }; + + /** + * Returns the scheme to use for the transport URLs. + * + * @api private + */ + + XHR.prototype.scheme = function () { + return this.socket.options.secure ? 'https' : 'http'; + }; + + /** + * Check if the XHR transports are supported + * + * @param {Boolean} xdomain Check if we support cross domain requests. + * @returns {Boolean} + * @api public + */ + + XHR.check = function (socket, xdomain) { + try { + var request = io.util.request(xdomain), + usesXDomReq = (global.XDomainRequest && request instanceof XDomainRequest), + socketProtocol = (socket && socket.options && socket.options.secure ? 'https:' : 'http:'), + isXProtocol = (global.location && socketProtocol != global.location.protocol); + if (request && !(usesXDomReq && isXProtocol)) { + return true; + } + } catch(e) {} + + return false; + }; + + /** + * Check if the XHR transport supports cross domain requests. + * + * @returns {Boolean} + * @api public + */ + + XHR.xdomainCheck = function (socket) { + return XHR.check(socket, true); + }; + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.htmlfile = HTMLFile; + + /** + * The HTMLFile transport creates a `forever iframe` based transport + * for Internet Explorer. Regular forever iframe implementations will + * continuously trigger the browsers buzy indicators. If the forever iframe + * is created inside a `htmlfile` these indicators will not be trigged. + * + * @constructor + * @extends {io.Transport.XHR} + * @api public + */ + + function HTMLFile (socket) { + io.Transport.XHR.apply(this, arguments); + }; + + /** + * Inherits from XHR transport. + */ + + io.util.inherit(HTMLFile, io.Transport.XHR); + + /** + * Transport name + * + * @api public + */ + + HTMLFile.prototype.name = 'htmlfile'; + + /** + * Creates a new Ac...eX `htmlfile` with a forever loading iframe + * that can be used to listen to messages. Inside the generated + * `htmlfile` a reference will be made to the HTMLFile transport. + * + * @api private + */ + + HTMLFile.prototype.get = function () { + this.doc = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); + this.doc.open(); + this.doc.write('<html></html>'); + this.doc.close(); + this.doc.parentWindow.s = this; + + var iframeC = this.doc.createElement('div'); + iframeC.className = 'socketio'; + + this.doc.body.appendChild(iframeC); + this.iframe = this.doc.createElement('iframe'); + + iframeC.appendChild(this.iframe); + + var self = this + , query = io.util.query(this.socket.options.query, 't='+ +new Date); + + this.iframe.src = this.prepareUrl() + query; + + io.util.on(window, 'unload', function () { + self.destroy(); + }); + }; + + /** + * The Socket.IO server will write script tags inside the forever + * iframe, this function will be used as callback for the incoming + * information. + * + * @param {String} data The message + * @param {document} doc Reference to the context + * @api private + */ + + HTMLFile.prototype._ = function (data, doc) { + this.onData(data); + try { + var script = doc.getElementsByTagName('script')[0]; + script.parentNode.removeChild(script); + } catch (e) { } + }; + + /** + * Destroy the established connection, iframe and `htmlfile`. + * And calls the `CollectGarbage` function of Internet Explorer + * to release the memory. + * + * @api private + */ + + HTMLFile.prototype.destroy = function () { + if (this.iframe){ + try { + this.iframe.src = 'about:blank'; + } catch(e){} + + this.doc = null; + this.iframe.parentNode.removeChild(this.iframe); + this.iframe = null; + + CollectGarbage(); + } + }; + + /** + * Disconnects the established connection. + * + * @returns {Transport} Chaining. + * @api public + */ + + HTMLFile.prototype.close = function () { + this.destroy(); + return io.Transport.XHR.prototype.close.call(this); + }; + + /** + * Checks if the browser supports this transport. The browser + * must have an `Ac...eXObject` implementation. + * + * @return {Boolean} + * @api public + */ + + HTMLFile.check = function (socket) { + if (typeof window != "undefined" && (['Active'].concat('Object').join('X')) in window){ + try { + var a = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); + return a && io.Transport.XHR.check(socket); + } catch(e){} + } + return false; + }; + + /** + * Check if cross domain requests are supported. + * + * @returns {Boolean} + * @api public + */ + + HTMLFile.xdomainCheck = function () { + // we can probably do handling for sub-domains, we should + // test that it's cross domain but a subdomain here + return false; + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('htmlfile'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports['xhr-polling'] = XHRPolling; + + /** + * The XHR-polling transport uses long polling XHR requests to create a + * "persistent" connection with the server. + * + * @constructor + * @api public + */ + + function XHRPolling () { + io.Transport.XHR.apply(this, arguments); + }; + + /** + * Inherits from XHR transport. + */ + + io.util.inherit(XHRPolling, io.Transport.XHR); + + /** + * Merge the properties from XHR transport + */ + + io.util.merge(XHRPolling, io.Transport.XHR); + + /** + * Transport name + * + * @api public + */ + + XHRPolling.prototype.name = 'xhr-polling'; + + /** + * Indicates whether heartbeats is enabled for this transport + * + * @api private + */ + + XHRPolling.prototype.heartbeats = function () { + return false; + }; + + /** + * Establish a connection, for iPhone and Android this will be done once the page + * is loaded. + * + * @returns {Transport} Chaining. + * @api public + */ + + XHRPolling.prototype.open = function () { + var self = this; + + io.Transport.XHR.prototype.open.call(self); + return false; + }; + + /** + * Starts a XHR request to wait for incoming messages. + * + * @api private + */ + + function empty () {}; + + XHRPolling.prototype.get = function () { + if (!this.isOpen) return; + + var self = this; + + function stateChange () { + if (this.readyState == 4) { + this.onreadystatechange = empty; + + if (this.status == 200) { + self.onData(this.responseText); + self.get(); + } else { + self.onClose(); + } + } + }; + + function onload () { + this.onload = empty; + this.onerror = empty; + self.retryCounter = 1; + self.onData(this.responseText); + self.get(); + }; + + function onerror () { + self.retryCounter ++; + if(!self.retryCounter || self.retryCounter > 3) { + self.onClose(); + } else { + self.get(); + } + }; + + this.xhr = this.request(); + + if (global.XDomainRequest && this.xhr instanceof XDomainRequest) { + this.xhr.onload = onload; + this.xhr.onerror = onerror; + } else { + this.xhr.onreadystatechange = stateChange; + } + + this.xhr.send(null); + }; + + /** + * Handle the unclean close behavior. + * + * @api private + */ + + XHRPolling.prototype.onClose = function () { + io.Transport.XHR.prototype.onClose.call(this); + + if (this.xhr) { + this.xhr.onreadystatechange = this.xhr.onload = this.xhr.onerror = empty; + try { + this.xhr.abort(); + } catch(e){} + this.xhr = null; + } + }; + + /** + * Webkit based browsers show a infinit spinner when you start a XHR request + * before the browsers onload event is called so we need to defer opening of + * the transport until the onload event is called. Wrapping the cb in our + * defer method solve this. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + XHRPolling.prototype.ready = function (socket, fn) { + var self = this; + + io.util.defer(function () { + fn.call(self); + }); + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('xhr-polling'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost <dev@learnboost.com> + * MIT Licensed + */ + +(function (exports, io) { + /** + * There is a way to hide the loading indicator in Firefox. If you create and + * remove a iframe it will stop showing the current loading indicator. + * Unfortunately we can't feature detect that and UA sniffing is evil. + * + * @api private + */ + + var indicator = global.document && "MozAppearance" in + global.document.documentElement.style; + + /** + * Expose constructor. + */ + + exports['jsonp-polling'] = JSONPPolling; + + /** + * The JSONP transport creates an persistent connection by dynamically + * inserting a script tag in the page. This script tag will receive the + * information of the Socket.IO server. When new information is received + * it creates a new script tag for the new data stream. + * + * @constructor + * @extends {io.Transport.xhr-polling} + * @api public + */ + + function JSONPPolling (socket) { + io.Transport['xhr-polling'].apply(this, arguments); + + this.index = io.j.length; + + var self = this; + + io.j.push(function (msg) { + self._(msg); + }); + }; + + /** + * Inherits from XHR polling transport. + */ + + io.util.inherit(JSONPPolling, io.Transport['xhr-polling']); + + /** + * Transport name + * + * @api public + */ + + JSONPPolling.prototype.name = 'jsonp-polling'; + + /** + * Posts a encoded message to the Socket.IO server using an iframe. + * The iframe is used because script tags can create POST based requests. + * The iframe is positioned outside of the view so the user does not + * notice it's existence. + * + * @param {String} data A encoded message. + * @api private + */ + + JSONPPolling.prototype.post = function (data) { + var self = this + , query = io.util.query( + this.socket.options.query + , 't='+ (+new Date) + '&i=' + this.index + ); + + if (!this.form) { + var form = document.createElement('form') + , area = document.createElement('textarea') + , id = this.iframeId = 'socketio_iframe_' + this.index + , iframe; + + form.className = 'socketio'; + form.style.position = 'absolute'; + form.style.top = '0px'; + form.style.left = '0px'; + form.style.display = 'none'; + form.target = id; + form.method = 'POST'; + form.setAttribute('accept-charset', 'utf-8'); + area.name = 'd'; + form.appendChild(area); + document.body.appendChild(form); + + this.form = form; + this.area = area; + } + + this.form.action = this.prepareUrl() + query; + + function complete () { + initIframe(); + self.socket.setBuffer(false); + }; + + function initIframe () { + if (self.iframe) { + self.form.removeChild(self.iframe); + } + + try { + // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) + iframe = document.createElement('<iframe name="'+ self.iframeId +'">'); + } catch (e) { + iframe = document.createElement('iframe'); + iframe.name = self.iframeId; + } + + iframe.id = self.iframeId; + + self.form.appendChild(iframe); + self.iframe = iframe; + }; + + initIframe(); + + // we temporarily stringify until we figure out how to prevent + // browsers from turning `\n` into `\r\n` in form inputs + this.area.value = io.JSON.stringify(data); + + try { + this.form.submit(); + } catch(e) {} + + if (this.iframe.attachEvent) { + iframe.onreadystatechange = function () { + if (self.iframe.readyState == 'complete') { + complete(); + } + }; + } else { + this.iframe.onload = complete; + } + + this.socket.setBuffer(true); + }; + + /** + * Creates a new JSONP poll that can be used to listen + * for messages from the Socket.IO server. + * + * @api private + */ + + JSONPPolling.prototype.get = function () { + var self = this + , script = document.createElement('script') + , query = io.util.query( + this.socket.options.query + , 't='+ (+new Date) + '&i=' + this.index + ); + + if (this.script) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } + + script.async = true; + script.src = this.prepareUrl() + query; + script.onerror = function () { + self.onClose(); + }; + + var insertAt = document.getElementsByTagName('script')[0]; + insertAt.parentNode.insertBefore(script, insertAt); + this.script = script; + + if (indicator) { + setTimeout(function () { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + document.body.removeChild(iframe); + }, 100); + } + }; + + /** + * Callback function for the incoming message stream from the Socket.IO server. + * + * @param {String} data The message + * @api private + */ + + JSONPPolling.prototype._ = function (msg) { + this.onData(msg); + if (this.isOpen) { + this.get(); + } + return this; + }; + + /** + * The indicator hack only works after onload + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + JSONPPolling.prototype.ready = function (socket, fn) { + var self = this; + if (!indicator) return fn.call(this); + + io.util.load(function () { + fn.call(self); + }); + }; + + /** + * Checks if browser supports this transport. + * + * @return {Boolean} + * @api public + */ + + JSONPPolling.check = function () { + return 'document' in global; + }; + + /** + * Check if cross domain requests are supported + * + * @returns {Boolean} + * @api public + */ + + JSONPPolling.xdomainCheck = function () { + return true; + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('jsonp-polling'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +if (typeof define === "function" && define.amd) { + define([], function () { return io; }); +} else { + return io; +} + +})(window); diff --git a/src/wrtcp.js b/src/wrtcp.js new file mode 100644 index 00000000..c23c95d7 --- /dev/null +++ b/src/wrtcp.js @@ -0,0 +1,855 @@ + +(function() { + + // Some functions to help support binary messages for Chrome until there is native support + + function byteLength(string, optEncoding) { + // FIXME: support other encodings + return window.unescape(encodeURIComponent(string)).length; + }; + + /** + * [source]http://closure-library.googlecode.com/svn/docs/closure_goog_crypt_crypt.js.source.html + * Converts a JS string to a UTF-8 "byte" array. + * @param {string} str 16-bit unicode string. + * @return {Array.<number>} UTF-8 byte array. + */ + function stringToUtf8ByteArray(string, offset, length, buffer) { + // TODO(user): Use native implementations if/when available + string = string.replace(/\r\n/g, '\n'); + var p = 0; + for (var i = offset; i < string.length && p < length; i++) { + var c = string.charCodeAt(i); + if (c < 128) { + buffer[p++] = c; + } else if (c < 2048) { + buffer[p++] = (c >> 6) | 192; + buffer[p++] = (c & 63) | 128; + } else { + buffer[p++] = (c >> 12) | 224; + buffer[p++] = ((c >> 6) & 63) | 128; + buffer[p++] = (c & 63) | 128; + } + } + return buffer; + } + + /** + * [source]http://closure-library.googlecode.com/svn/docs/closure_goog_crypt_crypt.js.source.html + * Converts a UTF-8 byte array to JavaScript's 16-bit Unicode. + * @param {Array.<number>} bytes UTF-8 byte array. + * @return {string} 16-bit Unicode string. + */ + function utf8ByteArrayToString(bytes) { + // TODO(user): Use native implementations if/when available + var out = [], pos = 0, c = 0; + var c1, c2, c3; + while (pos < bytes.length) { + c1 = bytes[pos++]; + if (c1 < 128) { + out[c++] = String.fromCharCode(c1); + } else if (c1 > 191 && c1 < 224) { + c2 = bytes[pos++]; + out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63); + } else { + c2 = bytes[pos++]; + c3 = bytes[pos++]; + out[c++] = String.fromCharCode( + (c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63); + } + } + return out.join(''); + } + + /* Notes + * + * - Continue using prefixed names for now. + * + */ + + var webrtcSupported = true; + + var RTCPeerConnection; + if(window.mozRTCPeerConnection) + RTCPeerConnection = window.mozRTCPeerConnection; + else if(window.webkitRTCPeerConnection) + RTCPeerConnection = window.webkitRTCPeerConnection; + else if(window.RTCPeerConnection) + RTCPeerConnection = window.RTCPeerConnection + else + webrtcSupported = false; + + var RTCSessionDescription; + if(window.mozRTCSessionDescription) + RTCSessionDescription = window.mozRTCSessionDescription; + else if(window.webkitRTCSessionDescription) + RTCSessionDescription = window.webkitRTCSessionDescription; + else if(window.RTCSessionDescription) + RTCSessionDescription = window.RTCSessionDescription + else + webrtcSupported = false; + + var RTCIceCandidate; + if(window.mozRTCIceCandidate) + RTCIceCandidate = window.mozRTCIceCandidate; + else if(window.webkitRTCIceCandidate) + RTCIceCandidate = window.webkitRTCIceCandidate; + else if(window.RTCIceCandidate) + RTCIceCandidate = window.RTCIceCandidate; + else + webrtcSupported = false; + + var getUserMedia; + if(!navigator.getUserMedia) { + if(navigator.mozGetUserMedia) + getUserMedia = navigator.mozGetUserMedia.bind(navigator); + else if(navigator.webkitGetUserMedia) + getUserMedia = navigator.webkitGetUserMedia.bind(navigator); + else + webrtcSupported = false; + } else { + getUserMedia = navigator.getUserMedia.bind(navigator); + } + + // FIXME: browser detection is gross, but I don't see another way to do this + var RTCConnectProtocol; + if(window.mozRTCPeerConnection) { + RTCConnectProtocol = mozRTCConnectProtocol; + } else if(window.webkitRTCPeerConnection) { + RTCConnectProtocol = webkitRTCConnectProtocol; + } else { + webrtcSupported = false; + } + + function callback(object, method, args) { + if(!Array.isArray(args)) + args = [args]; + if(method in object && 'function' === typeof object[method]) { + object[method].apply(object, args); + } + }; + + function fail(object, method, error) { + if (!(error instanceof Error)) + error = new Error(error); + callback(object, method, [error]); + }; + + function defer(queue, object, method, args) { + if(queue) { + queue.push([object, method, args]); + return true; + } else { + return false; + } + }; + + function processDeferredQueue(queue) { + while(queue.length) { + var deferred = queue.shift(); + callback(deferred[0], deferred[1], deferred[2]); + } + }; + + var ONE_SECOND = 1000; // milliseconds + var DEFAULT_CONNECTION_TIMEOUT = 10 * ONE_SECOND; + var DEFAULT_PING_TIMEOUT = 1 * ONE_SECOND; + var RELIABLE_CHANNEL_OPTIONS = { + reliable: false + }; + var UNRELIABLE_CHANNEL_OPTIONS = { + outOfOrderAllowed: true, + maxRetransmitNum: 0, + reliable: false + }; + + function PendingConnectionAbortError(message) { + this.name = "PendingConnectionAbortError"; + this.message = (message || ""); + }; + PendingConnectionAbortError.prototype = Error.prototype; + + function ConnectionFailedError(message) { + this.name = "ConnectionFailedError"; + this.message = (message || ""); + }; + ConnectionFailedError.prototype = Error.prototype; + + var E = { + PendingConnectionAbortError: PendingConnectionAbortError, + ConnectionFailedError: ConnectionFailedError + }; + + function WebSocketBroker(brokerUrl) { + this.brokerUrl = brokerUrl; + this.state = WebSocketBroker.OFFLINE; + + this.onstatechange = null; + this.onreceive = null; + this.onerror = null; + + this.socket = null; + this.route = null; + }; + + // States + WebSocketBroker.OFFLINE = 0x00; + WebSocketBroker.CONNECTING = 0x01; + WebSocketBroker.CONNECTED = 0x02; + // Flags + WebSocketBroker.ROUTED = 0x10; + WebSocketBroker.LISTENING = 0x20; + + WebSocketBroker.prototype.setState = function setState(state, clearFlags) { + var clear = clearFlags ? 0x00 : 0xF0; + this.state &= clear >>> 0; + this.state |= state >>> 0; + callback(this, 'onstatechange', [this.state, (state | (clear & 0x0)) >>> 0]); + }; + WebSocketBroker.prototype.setFlag = function setFlag(flag) { + this.state = (this.state | flag) >>> 0; + callback(this, 'onstatechange', [this.state, flag]) + }; + WebSocketBroker.prototype.clearFlag = function clearFlag(flag) { + flag = (~flag) >>> 0; + this.state = (this.state & flag) >>> 0; + callback(this, 'onstatechange', [this.state, flag]) + }; + WebSocketBroker.prototype.checkState = function checkState(mask) { + return !!(this.state & mask); + }; + WebSocketBroker.prototype.connect = function connect() { + var that = this; + var socket = io.connect(this.brokerUrl + '/peer', { + 'sync disconnect on unload': true // partially fixes 'interrupted while page loading' warning + }); + + socket.on('connecting', function onconnecting() { + that.setState(WebSocketBroker.CONNECTING, true); + }); + + socket.on('connect', function onconnect() { + that.setState(WebSocketBroker.CONNECTED, true); + }); + + socket.on('connect_failed', function onconnect_failed() { + that.setState(WebSocketBroker.OFFLINE, true); + }); + + socket.on('route', function onroute(route) { + that.route = route; + that.setFlag(WebSocketBroker.ROUTED); + }); + + socket.on('disconnect', function ondisconnect() { + that.setState(WebSocketBroker.OFFLINE, true); + }); + + socket.on('error', function onerror(error) { + fail(that, 'onerror', error); + }); + + socket.on('receive', function onreceive(message) { + var from = message['from']; + var data = message['data']; + callback(that, 'onreceive', [from, data]); + }); + + this.socket = socket; + }; + WebSocketBroker.prototype.disconnect = function disconnect() { + if(this.checkState(WebSocketBroker.CONNECTED)) { + this.socket.disconnect(); + this.setState(WebSocketBroker.OFFLINE, true); + return true; + } else { + return false; + } + }; + WebSocketBroker.prototype.listen = function listen(options) { + var that = this; + if(this.checkState(WebSocketBroker.CONNECTED)) { + this.socket.emit('listen', options, function onresponse(response) { + if(response && response['error']) { + var error = new Error(response['error']); + fail(that, 'onerror', error); + } else { + that.setFlag(WebSocketBroker.LISTENING); + } + }); + } + }; + WebSocketBroker.prototype.ignore = function ignore() { + var that = this; + if(this.checkState(WebSocketBroker.CONNECTED)) { + this.socket.emit('ignore', null, function onresponse(response) { + if(response && response['error']) { + var error = new Error(response['error']); + fail(that, 'onerror', error) + } else { + that.clearFlag(WebSocketBroker.LISTENING); + } + }); + } + }; + WebSocketBroker.prototype.send = function send(to, message) { + var that = this; + if(this.checkState(WebSocketBroker.CONNECTED)) { + this.socket.emit('send', {'to': to, 'data': message}, function onresponse(response) { + if(response && response['error']) { + var error = new Error(response['error']); + fail(that, 'onerror', error) + } + }); + }; + }; + + var dataChannels = { + 'reliable': 'RELIABLE', + 'unreliable': 'UNRELIABLE', + '@control': 'RELIABLE' + }; + var nextDataConnectionPort = 1; + function CommonRTCConnectProtocol() { + // FIXME: these timeouts should be configurable + this.connectionTimeout = 10 * ONE_SECOND; + this.pingTimeout = 1 * ONE_SECOND; + }; + CommonRTCConnectProtocol.prototype.process = function process(message) { + var that = this; + + var type = message['type']; + switch(type) { + case 'ice': + var candidate = JSON.parse(message['candidate']); + if(candidate) + this.handleIce(candidate); + break; + + case 'offer': + that.ports.remote = message['port']; + var offer = { + 'type': 'offer', + 'sdp': message['description'] + }; + this.handleOffer(offer); + break; + + case 'answer': + that.ports.remote = message['port']; + var answer = { + 'type': 'answer', + 'sdp': message['description'] + }; + this.handleAnswer(answer); + break; + + case 'abort': + this.handleAbort(); + break; + + default: + fail(this, 'onerror', 'unknown message'); + } + }; + CommonRTCConnectProtocol.prototype.handleAbort = function handleAbort() { + fail(this, 'onerror', new Error(E.RTCConnectProtocolAbort)); + }; + CommonRTCConnectProtocol.prototype.initialize = function initialize(cb) { + var that = this; + + if(this.peerConnection) + return cb(); + + // FIXME: peer connection servers should be configurable + this.peerConnection = new RTCPeerConnection(this.connectionServers, this.connectionOptions); + this.peerConnection.onicecandidate = function(event) { + var message = { + 'type': 'ice', + 'candidate': JSON.stringify(event.candidate) + }; + callback(that, 'onmessage', message); + }; + this.peerConnection.onaddstream = function(event) { + that.streams['remote'] = event.stream; + }; + this.peerConnection.onstatechange = function(event) { + console.log(event.target.readyState); + }; + + function createStream(useFake) { + useFake = (!useVideo && !useAudio) ? true : useFake; + var useVideo = !!that.options['video']; + var useAudio = !!that.options['audio']; + var mediaOptions = { + video: useVideo, + audio: (!useVideo && !useAudio) ? true : useAudio, + fake: useFake + }; + getUserMedia(mediaOptions, + function(stream) { + that.peerConnection.addStream(stream); + that.streams['local'] = stream; + cb(); + }, + function(error) { + console.error('!', error); + if(!useFake) + createStream(true); + else + fail(that, 'onerror', error); + } + ); + } + + createStream(); + }; + CommonRTCConnectProtocol.prototype.handleIce = function handleIce(candidate) { + var that = this; + + function setIce() { + if(!that.peerConnection.remoteDescription) { + return + } + that.peerConnection.addIceCandidate(new RTCIceCandidate(candidate), + function(error) { + fail(that, 'onerror', error); + } + ); + }; + + this.initialize(setIce); + }; + CommonRTCConnectProtocol.prototype.initiate = function initiate() { + var that = this; + this.initiator = true; + + function createDataChannels() { + var labels = Object.keys(dataChannels); + labels.forEach(function(label) { + var channelOptions = that.channelOptions[dataChannels[label]]; + var channel = that._pending[label] = that.peerConnection.createDataChannel(label, channelOptions); + channel.binaryType = that.options['binaryType']; + channel.onopen = function() { + that.channels[label] = channel; + delete that._pending[label]; + if(Object.keys(that.channels).length === labels.length) { + that.complete = true; + callback(that, 'oncomplete', []); + } + }; + channel.onerror = function(error) { + console.error(error); + fail(that, 'onerror', error); + }; + }); + createOffer(); + }; + + function createOffer() { + that.peerConnection.createOffer(setLocal, + function(error) { + fail(that, 'onerror', error); + } + ); + }; + + function setLocal(description) { + that.peerConnection.setLocalDescription(new RTCSessionDescription(description), complete, + function(error) { + fail(that, 'onerror', error); + } + ); + + function complete() { + var message = { + 'type': 'offer', + 'description': description['sdp'], + 'port': that.ports.local + }; + callback(that, 'onmessage', message); + }; + }; + + this.initialize(createDataChannels); + }; + CommonRTCConnectProtocol.prototype.handleOffer = function handleOffer(offer) { + var that = this; + + function handleDataChannels() { + var labels = Object.keys(dataChannels); + that.peerConnection.ondatachannel = function(event) { + var channel = event.channel; + var label = channel.label; + that._pending[label] = channel; + channel.binaryType = that.options['binaryType']; + channel.onopen = function() { + that.channels[label] = channel; + delete that._pending[label]; + if(Object.keys(that.channels).length === labels.length) { + that.complete = true; + callback(that, 'oncomplete', []); + } + }; + channel.onerror = function(error) { + console.error(error); + fail(that, 'onerror', error); + }; + }; + setRemote(); + }; + + function setRemote() { + that.peerConnection.setRemoteDescription(new RTCSessionDescription(offer), createAnswer, + function(error) { + fail(that, 'onerror', error); + } + ); + }; + + function createAnswer() { + that.peerConnection.createAnswer(setLocal, + function(error) { + fail(that, 'onerror', error); + } + ); + }; + + function setLocal(description) { + that.peerConnection.setLocalDescription(new RTCSessionDescription(description), complete, + function(error) { + fail(that, 'onerror', error); + } + ); + + function complete() { + var message = { + 'type': 'answer', + 'description': description['sdp'], + 'port': that.ports.local + }; + callback(that, 'onmessage', message); + }; + }; + + this.initialize(handleDataChannels); + }; + CommonRTCConnectProtocol.prototype.handleAnswer = function handleAnswer(answer) { + var that = this; + + function setRemote() { + that.peerConnection.setRemoteDescription(new RTCSessionDescription(answer), complete, + function(error) { + fail(that, 'onerror', error); + } + ); + }; + + function complete() { + }; + + this.initialize(setRemote); + }; + + function mozRTCConnectProtocol(options) { + this.options = options; + this.onmessage = null; + this.oncomplete = null; + this.onerror = null; + + this.complete = false; + this.ports = { + local: nextDataConnectionPort ++, + remote: null + }; + this.streams = { + local: null, + remote: null + }; + this.initiator = false; + + this.peerConnection = null; + this.channels = {}; + this._pending = {}; + this.connectionServers = null; + this.connectionOptions = null; + this.channelOptions = { + RELIABLE: { + // defaults + }, + UNRELIABLE: { + outOfOrderAllowed: true, + maxRetransmitNum: 0 + } + }; + }; + mozRTCConnectProtocol.prototype = new CommonRTCConnectProtocol(); + mozRTCConnectProtocol.prototype.constructor = mozRTCConnectProtocol; + + function webkitRTCConnectProtocol(options) { + this.options = options; + this.onmessage = null; + this.oncomplete = null; + this.onerror = null; + + this.complete = false; + this.ports = { + local: nextDataConnectionPort ++, + remote: null + }; + this.streams = { + local: null, + remote: null + }; + this.initiator = false; + + this.peerConnection = null; + this.channels = {}; + this._pending = {}; + this.connectionServers = {iceServers:[{url:'stun:23.21.150.121'}]}; + this.connectionOptions = { + 'optional': [{ 'RtpDataChannels': true }] + }; + this.channelOptions = { + RELIABLE: { + // FIXME: reliable channels do not work in chrome yet + reliable: false + }, + UNRELIABLE: { + reliable: false + } + }; + }; + webkitRTCConnectProtocol.prototype = new CommonRTCConnectProtocol(); + webkitRTCConnectProtocol.prototype.constructor = webkitRTCConnectProtocol; + + // FIXME: this could use a cleanup + var nextConnectionId = 1; + function Connection(options, peerConnection, streams, channels) { + var that = this; + this.id = nextConnectionId ++; + this.streams = streams; + this.connected = false; + this.messageFlag = false; + + this.onmessage = null; + this.ondisconnect = null; + this.onerror = null; + + this.peerConnection = peerConnection; + + // DataChannels + this.channels = channels; + + this.connectionTimer = null; + this.pingTimer = null; + + function handleConnectionTimerExpired() { + if(!that.connected) + return + this.connectionTimer = null; + if(false === that.messageFlag) { + that.channels['@control'].send('ping'); + this.pingTimer = window.setTimeout(handlePingTimerExpired, options['pingTimeout']); + } else { + that.messageFlag = false; + this.connectionTimer = window.setTimeout(handleConnectionTimerExpired, options['connectionTimeout']); + } + }; + function handlePingTimerExpired() { + if(!that.connected) + return + this.pingTimer = null; + if(false === that.messageFlag) { + that.connected = false; + that.close(); + } else { + that.messageFlag = false; + this.connectionTimer = window.setTimeout(handleConnectionTimerExpired, options['connectionTimeout']); + } + }; + + Object.keys(this.channels).forEach(function(label) { + var channel = that.channels[label]; + if(label.match('^@')) // check for internal channels + return; + + channel.onmessage = function onmessage(message) { + that.messageFlag = true; + callback(that, 'onmessage', [label, message]); + }; + }); + this.channels['@control'].onmessage = function onmessage(message) { + that.messageFlag = true; + if(that.connected) { + var data = message.data; + if('ping' === data) { + that.channels['@control'].send('pong'); + } else if('pong' === data) { + // ok + } else if('quit' === data) { + that.close(); + } + } + }; + + this.connected = true; + this.connectionTimer = window.setTimeout(handleConnectionTimerExpired, options['connectionTimeout']); + }; + Connection.prototype.close = function close() { + console.log('close connection'); + if(this.connected) { + this.channels['@control'].send('quit'); + } + this.connected = false; + this.peerConnection.close(); + if(this.connectionTimer) { + window.clearInterval(this.connectionTimer); + this.connectionTimer = null; + } + if(this.pingTimer) { + window.clearInterval(this.pingTimer); + this.pingTimer = null; + } + this.peerConnection = null; + callback(this, 'ondisconnect', []); + }; + Connection.prototype.send = function send(label, message) { + this.channels[label].send(message); + }; + + function PendingConnection(route, incoming) { + this.route = route; + this.incoming = incoming; + this.proceed = true; + }; + PendingConnection.prototype.accept = function accept() { + this.proceed = true; + }; + PendingConnection.prototype.reject = function reject() { + this.proceed = false; + }; + + function Peer(brokerUrl, options) { + if(!webrtcSupported) + throw new Error("WebRTC not supported"); + + var that = this; + this.brokerUrl = brokerUrl; + this.options = options = options || {}; + options['binaryType'] = options['binaryType'] || 'arraybuffer'; + options['connectionTimeout'] = options['connectionTimeout'] || 10 * ONE_SECOND; + options['pingTimeout'] = options['pingTimeout'] || 1 * ONE_SECOND; + + this.onconnection = null; + this.onpending = null; + this.onroute = null; + this.onerror = null; + + this.broker = new WebSocketBroker(brokerUrl); + this.pending = {}; + + this.queues = { + connected: [], + listening: [] + }; + + this.broker.onstatechange = function onstatechange(state, mask) { + if(that.queues.connected.length && that.broker.checkState(WebSocketBroker.ROUTED)) { + processDeferredQueue(that.queues.connected); + if(that.queues.listening.length && that.broker.checkState(WebSocketBroker.LISTENING)) { + processDeferredQueue(that.queues.listening); + } + } + if(mask & WebSocketBroker.ROUTED) { + callback(that, 'onroute', that.broker.route); + } + }; + + this.broker.onreceive = function onreceive(from, message) { + var handshake; + if(!that.pending.hasOwnProperty(from)) { + if(!that.broker.checkState(WebSocketBroker.LISTENING)) { + return; + } + + var pendingConnection = new PendingConnection(from, /*incoming*/ true); + callback(that, 'onpending', [pendingConnection]); + if(!pendingConnection['proceed']) + return; + + var handshake = that.pending[from] = new RTCConnectProtocol(that.options); + handshake.oncomplete = function() { + var connection = new Connection(that.options, handshake.peerConnection, handshake.streams, handshake.channels); + connection['route'] = from; + delete that.pending[from]; + callback(that, 'onconnection', [connection]); + }; + handshake.onmessage = function(message) { + that.broker.send(from, message); + }; + handshake.onerror = function(error) { + delete that.pending[from]; + callback(that, 'onerror', [error]); + }; + } else { + handshake = that.pending[from]; + } + handshake.process(message); + }; + + this.broker.connect(); + }; + Peer.prototype.listen = function listen(options) { + if(!this.broker.checkState(WebSocketBroker.ROUTED)) + return defer(this.queues.connected, this, 'listen', [options]); + + options = options || {}; + options['url'] = options['url'] || window.location.toString(); + options['listed'] = (undefined !== options['listed']) ? options['listed'] : true; + options['metadata'] = options['metadata'] || {}; + + this.broker.listen(options); + }; + Peer.prototype.ignore = function ignore() { + throw new Error('not implemented'); + }; + Peer.prototype.connect = function connect(route) { + if(!this.broker.checkState(WebSocketBroker.ROUTED)) + return defer(this.queues.connected, this, 'connect', [route]); + + var that = this; + + if(this.pending.hasOwnProperty(route)) + throw new Error('already connecting to this host'); // FIXME: we can handle this better + + var pendingConnection = new PendingConnection(route, /*incoming*/ false); + callback(that, 'onpending', [pendingConnection]); + if(!pendingConnection['proceed']) + return; + + var handshake = this.pending[route] = new RTCConnectProtocol(this.options); + handshake.oncomplete = function() { + var connection = new Connection(this.options, handshake.peerConnection, handshake.streams, handshake.channels); + connection['route'] = route; + delete that.pending[route]; + callback(that, 'onconnection', [connection]); + }; + handshake.onmessage = function(message) { + that.broker.send(route, message); + }; + handshake.onerror = function(error) { + delete that.pending[route]; + fail(that, 'onerror', error); + }; + + handshake.initiate(); + }; + Peer.prototype.close = function close() { + this.broker.disconnect(); + }; + Peer.E = E; + + return Peer; + +})();
\ No newline at end of file |