├── .npmignore ├── .travis.yml ├── .gitignore ├── .editorconfig ├── dependencies ├── README.md ├── amd_instructions.js └── socket.io.min.js ├── example ├── server.js └── index.html ├── bower.json ├── Gruntfile.js ├── LICENSE ├── test ├── helpers │ ├── setupRoutes.js │ └── lifecycle.js ├── query.test.js ├── basic.test.js ├── websocket-only.test.js └── queue.test.js ├── package.json ├── README.md └── sails.io.js /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example 3 | dist 4 | dependencies 5 | Gruntfile.js 6 | README.md 7 | LICENSE 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | 6 | # whitelisted branches 7 | branches: 8 | only: 9 | - master 10 | - stable 11 | - beta 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /dependencies/README.md: -------------------------------------------------------------------------------- 1 | ## Why not use bower? 2 | 3 | I really, really want to use bower. But I also don't want to clone the entire repository of every possible dependency every time. 4 | 5 | A possible solution: 6 | https://www.npmjs.org/package/bower-installer 7 | 8 | For now, dependencies are included manually. -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To view this example, run `node server`, 3 | * then visit http://localhost:1337/example in your browser. 4 | */ 5 | require('sails').lift({ 6 | paths: { public: '../' } 7 | }, function (err) { 8 | if (err) throw err; 9 | 10 | console.log( 11 | '\n\n', 12 | 'Please visit', 13 | 'http://localhost:1337/example', 14 | 'in your browser to run this example.'); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /dependencies/amd_instructions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * To use sails.io.js in an AMD environment (e.g. with require.js), 3 | * replace this file with the sails.io.js file from the root of: 4 | * https://github.com/balderdashy/sails.io.js 5 | * and download a standalone copy of socket.io-client from: 6 | * https://github.com/socketio/socket.io-client 7 | * then follow the instructions at: 8 | * https://github.com/balderdashy/sails.io.js#requirejsamd-usage 9 | */ 10 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails.io.js", 3 | "main": "dist/sails.io.js", 4 | "version": "0.11.7", 5 | "homepage": "https://github.com/balderdashy/sails.io.js", 6 | "authors": [ 7 | "Mike McNeil " 8 | ], 9 | "description": "JavaScript Client SDK for Sails", 10 | "keywords": [ 11 | "sails", 12 | "socket.io", 13 | "sdk", 14 | "client", 15 | "sockets", 16 | "websockets" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "dependencies", 24 | "example", 25 | "Gruntfile.js", 26 | ".*", 27 | "bower.json", 28 | ".bower.json", 29 | "LICENSE", 30 | "README.md", 31 | "dependencies", 32 | "example", 33 | "package.json", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grunt automation. 3 | */ 4 | module.exports = function(grunt) { 5 | 6 | grunt.initConfig({ 7 | 8 | PATHS: { 9 | amdInstructions: './dependencies/amd_instructions.js', 10 | socketioClient: './dependencies/socket.io.min.js', 11 | sailsio: './sails.io.js', 12 | dist: './dist/sails.io.js' 13 | }, 14 | 15 | concat: { 16 | options: { 17 | separator: ';\n\n', 18 | }, 19 | dev: { 20 | src: ['<%= PATHS.amdInstructions %>', '<%= PATHS.socketioClient %>', '<%= PATHS.sailsio %>'], 21 | dest: '<%= PATHS.dist %>' 22 | } 23 | }, 24 | 25 | watch: { 26 | files: ['<%= PATHS.sailsio %>'], 27 | tasks: ['concat'] 28 | } 29 | }); 30 | 31 | grunt.loadNpmTasks('grunt-contrib-concat'); 32 | grunt.loadNpmTasks('grunt-contrib-watch'); 33 | 34 | grunt.registerTask('default', ['dev']); 35 | 36 | // Dev enviroment for copying over changes from src to example project. 37 | grunt.registerTask('dev', ['concat', 'watch']); 38 | 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Balderdash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/helpers/setupRoutes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('lodash'); 6 | 7 | //Helper function we use to do a lookup on an object. 8 | //It splits the dot string to an object path. 9 | 10 | function _dotToObject(obj, path) { 11 | return path.split('.').reduce(function objectIndex(obj, i) { 12 | return obj[i] 13 | }, obj); 14 | } 15 | 16 | /** 17 | * @return {Function} configured and ready to use by mocha's `before()` 18 | */ 19 | 20 | module.exports = function setupRoutes (expectedResponses) { 21 | 22 | /** 23 | * Bind routes which respond with the expected data. 24 | * 25 | * @param {Object} expectedResponses 26 | * @global {Sails} sails 27 | */ 28 | return function configuredFn () { 29 | _.each(expectedResponses, function (expectedResponse, routeAddress) { 30 | sails.router.bind(routeAddress, function (req, res) { 31 | // console.log('\n------ calling res.send(%s, "%s")', expectedResponse.statusCode || 200, expectedResponse.body); 32 | return res.send(expectedResponse.statusCode || 200, expectedResponse.req && _dotToObject(req, expectedResponse.req) || expectedResponse.body); 33 | }); 34 | }); 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails.io.js", 3 | "version": "0.11.7", 4 | "description": "Javascript SDK for communicating w/ a Sails server via WebSockets/socket.io.", 5 | "main": "sails.io.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": { 10 | "request": "~2.34.0" 11 | }, 12 | "devDependencies": { 13 | "grunt": "~0.4.3", 14 | "grunt-contrib-concat": "~0.3.0", 15 | "grunt-contrib-watch": "~0.6.0", 16 | "grunt-contrib-copy": "~0.5.0", 17 | "sails": "~0.11.0", 18 | "lodash": "~2.4.1", 19 | "mocha": "~1.18.0", 20 | "socket.io-client": "~1.2.1", 21 | "async": "~0.9.0" 22 | }, 23 | "scripts": { 24 | "test": "node ./node_modules/mocha/bin/mocha -b --reporter spec --timeout 10000" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/balderdashy/sails.io.js.git" 29 | }, 30 | "keywords": [ 31 | "sails", 32 | "sdk", 33 | "sails.io.js", 34 | "socket.io", 35 | "browser", 36 | "javascript" 37 | ], 38 | "author": "Mike McNeil <@mikermcneil> (http://balderdash.co/)", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/balderdashy/sails.io.js/issues" 42 | }, 43 | "homepage": "https://github.com/balderdashy/sails.io.js" 44 | } 45 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Sails Socket SDK example (browser)

7 | (check out the javascript console) 8 | 9 | 10 | 11 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/query.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'); 6 | var assert = require('assert'); 7 | var lifecycle = require('./helpers/lifecycle'); 8 | var _setupRoutes = require('./helpers/setupRoutes'); 9 | var _assertResponse = function ( expectedResponses ) { 10 | return function (routeAddress, callbackArgs) { 11 | var body = callbackArgs[0]; 12 | var jwr = callbackArgs[1]; 13 | 14 | // Ensure JWR is valid 15 | assert.equal(typeof jwr, 'object'); 16 | 17 | // Ensure body's type is correct 18 | assert.equal((typeof body),(typeof expectedResponses[routeAddress].body), util.format('Expecting type:%s:\n%s\n\nbut got type:%s:\n%s\n', (typeof expectedResponses[routeAddress].body), util.inspect(expectedResponses[routeAddress].body, false, null), (typeof body),util.inspect(body,false,null))); 19 | 20 | // Ensure body is the correct value 21 | assert.deepEqual(expectedResponses[routeAddress].body, body); 22 | 23 | // Ensure jwr's statusCode is correct 24 | assert.deepEqual(expectedResponses[routeAddress].statusCode || 200, jwr.statusCode); 25 | }; 26 | }; 27 | 28 | var EXPECTED_RESPONSES = { 29 | 'post /auth': { req: 'socket.handshake.query.token', body: '290891' } 30 | }; 31 | 32 | var setupRoutes = _setupRoutes(EXPECTED_RESPONSES); 33 | var assertResponse = _assertResponse(EXPECTED_RESPONSES); 34 | 35 | 36 | describe('io.socket', function () { 37 | 38 | describe('With query settings', function() { 39 | before(function(done) { 40 | lifecycle.setup({transports: ['websocket'], query: 'token=290891'}, function() { 41 | done(); 42 | }); 43 | }); 44 | before(setupRoutes); 45 | 46 | describe('once connected, socket', function () { 47 | 48 | it('should be able to send a query parameter and receive the expected query in the request', function (cb) { 49 | io.socket.post('/auth', {}, function (body, jwr) { 50 | assertResponse('post /auth', arguments); 51 | return cb(); 52 | }); 53 | }); 54 | 55 | }); 56 | 57 | 58 | after(lifecycle.teardown); 59 | 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'); 6 | var assert = require('assert'); 7 | var lifecycle = require('./helpers/lifecycle'); 8 | var _setupRoutes = require('./helpers/setupRoutes'); 9 | var _assertResponse = function ( expectedResponses ) { 10 | return function (routeAddress, callbackArgs) { 11 | var body = callbackArgs[0]; 12 | var jwr = callbackArgs[1]; 13 | 14 | // Ensure JWR is valid 15 | assert.equal(typeof jwr, 'object'); 16 | 17 | // Ensure body's type is correct 18 | assert.equal((typeof body),(typeof expectedResponses[routeAddress].body), util.format('Expecting type:%s:\n%s\n\nbut got type:%s:\n%s\n', (typeof expectedResponses[routeAddress].body), util.inspect(expectedResponses[routeAddress].body, false, null), (typeof body),util.inspect(body,false,null))); 19 | 20 | // Ensure body is the correct value 21 | assert.deepEqual(expectedResponses[routeAddress].body, body); 22 | 23 | // Ensure jwr's statusCode is correct 24 | assert.deepEqual(expectedResponses[routeAddress].statusCode || 200, jwr.statusCode); 25 | }; 26 | }; 27 | 28 | var EXPECTED_RESPONSES = { 29 | 'get /hello': { body: 'ok!' }, 30 | 'get /someJSON': { 31 | body: { foo: 'bar' } 32 | }, 33 | 'get /someError': { 34 | body: { blah: 'blah' }, 35 | statusCode: 501 36 | } 37 | }; 38 | var setupRoutes = _setupRoutes(EXPECTED_RESPONSES); 39 | var assertResponse = _assertResponse(EXPECTED_RESPONSES); 40 | 41 | 42 | describe('io.socket', function () { 43 | 44 | describe('With default settings', function() { 45 | before(lifecycle.setup); 46 | before(setupRoutes); 47 | 48 | it('should connect automatically', function (cb) { 49 | io.socket.on('connect', cb); 50 | }); 51 | 52 | describe('once connected, socket', function () { 53 | 54 | it('should be able to send a GET request and receive the expected response', function (cb) { 55 | io.socket.get('/hello', function (body, jwr) { 56 | assertResponse('get /hello', arguments); 57 | return cb(); 58 | }); 59 | }); 60 | 61 | it('should receive JSON as a POJO, not a string', function (cb) { 62 | io.socket.get('/someJSON', function (body, jwr) { 63 | assertResponse('get /someJSON', arguments); 64 | return cb(); 65 | }); 66 | }); 67 | 68 | it('should receive a valid jwr response object as its second argument, with the correct error code', function (cb) { 69 | io.socket.get('/someError', function (body, jwr) { 70 | assertResponse('get /someError', arguments); 71 | return cb(); 72 | }); 73 | }); 74 | 75 | }); 76 | 77 | 78 | after(lifecycle.teardown); 79 | 80 | }); 81 | 82 | }); 83 | -------------------------------------------------------------------------------- /test/websocket-only.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'); 6 | var assert = require('assert'); 7 | var lifecycle = require('./helpers/lifecycle'); 8 | var _setupRoutes = require('./helpers/setupRoutes'); 9 | var _assertResponse = function ( expectedResponses ) { 10 | return function (routeAddress, callbackArgs) { 11 | var body = callbackArgs[0]; 12 | var jwr = callbackArgs[1]; 13 | 14 | // Ensure JWR is valid 15 | assert.equal(typeof jwr, 'object'); 16 | 17 | // Ensure body's type is correct 18 | assert.equal((typeof body),(typeof expectedResponses[routeAddress].body), util.format('Expecting type:%s:\n%s\n\nbut got type:%s:\n%s\n', (typeof expectedResponses[routeAddress].body), util.inspect(expectedResponses[routeAddress].body, false, null), (typeof body),util.inspect(body,false,null))); 19 | 20 | // Ensure body is the correct value 21 | assert.deepEqual(expectedResponses[routeAddress].body, body); 22 | 23 | // Ensure jwr's statusCode is correct 24 | assert.deepEqual(expectedResponses[routeAddress].statusCode || 200, jwr.statusCode); 25 | }; 26 | }; 27 | 28 | var EXPECTED_RESPONSES = { 29 | 'get /hello': { body: 'ok!' }, 30 | 'get /someJSON': { 31 | body: { foo: 'bar' } 32 | }, 33 | 'get /someError': { 34 | body: { blah: 'blah' }, 35 | statusCode: 501 36 | } 37 | }; 38 | var setupRoutes = _setupRoutes(EXPECTED_RESPONSES); 39 | var assertResponse = _assertResponse(EXPECTED_RESPONSES); 40 | 41 | 42 | describe('io.socket', function () { 43 | 44 | describe('With transport: [\'websocket\']', function() { 45 | 46 | var socket; 47 | before(function(done) { 48 | lifecycle.setup({transports: ['websocket']}, function() { 49 | // socket = io.sails.connect(io.sails.url); 50 | done(); 51 | }); 52 | }); 53 | before(setupRoutes); 54 | 55 | describe('once connected, socket', function () { 56 | 57 | it('should be able to send a GET request and receive the expected response', function (cb) { 58 | io.socket.get('/hello', function (body, jwr) { 59 | assertResponse('get /hello', arguments); 60 | return cb(); 61 | }); 62 | }); 63 | 64 | it('should receive JSON as a POJO, not a string', function (cb) { 65 | io.socket.get('/someJSON', function (body, jwr) { 66 | assertResponse('get /someJSON', arguments); 67 | return cb(); 68 | }); 69 | }); 70 | 71 | it('should receive a valid jwr response object as its second argument, with the correct error code', function (cb) { 72 | io.socket.get('/someError', function (body, jwr) { 73 | assertResponse('get /someError', arguments); 74 | return cb(); 75 | }); 76 | }); 77 | 78 | }); 79 | 80 | 81 | after(lifecycle.teardown); 82 | 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /test/helpers/lifecycle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var Sails = require('sails/lib/app'); 6 | var _ = require('lodash'); 7 | 8 | // Use a weird port to avoid tests failing if we 9 | // forget to shut down another Sails app 10 | var TEST_SERVER_PORT = 1577; 11 | 12 | 13 | /** 14 | * @type {Object} 15 | */ 16 | module.exports = { 17 | 18 | setup: function (opts, cb) { 19 | 20 | // Invalidate socket.io-client in require cache 21 | _.each(_.keys(require.cache), function (modulePath) { 22 | if (modulePath.match(/socket.io-client/)){ 23 | delete require.cache[modulePath]; 24 | } 25 | }); 26 | 27 | // Require socket.io-client and sails.io.js fresh 28 | var io = require('socket.io-client'); 29 | var sailsIO = require('../../sails.io.js'); 30 | 31 | if (typeof opts == 'function') { 32 | cb = opts; 33 | opts = {}; 34 | } 35 | 36 | // New up an instance of Sails 37 | // and lift it. 38 | var app = Sails(); 39 | app.lift({ 40 | log: { level: 'error' }, 41 | port: TEST_SERVER_PORT, 42 | sockets: { 43 | authorization: false, 44 | transports: opts.transports 45 | } 46 | },function (err) { 47 | if (err) return cb(err); 48 | 49 | // Instantiate socket client. 50 | io = sailsIO(io); 51 | // Set some options. 52 | io.sails.url = opts.url || 'http://localhost:'+TEST_SERVER_PORT; 53 | // Disable the sails.io.js client's logger 54 | io.sails.environment = opts.environment || 'production'; 55 | 56 | if (typeof (opts.multiplex) != 'undefined') { 57 | io.sails.multiplex = opts.multiplex; 58 | } 59 | 60 | if (typeof (opts.transports) != 'undefined') { 61 | io.sails.transports = opts.transports; 62 | } 63 | 64 | if (typeof (opts.autoConnect) != 'undefined') { 65 | io.sails.autoConnect = opts.autoConnect; 66 | } 67 | 68 | if (typeof (opts.useCORSRouteToGetCookie) != 'undefined') { 69 | io.sails.useCORSRouteToGetCookie = opts.useCORSRouteToGetCookie; 70 | } 71 | 72 | if (typeof (opts.query) != 'undefined') { 73 | io.sails.query = opts.query; 74 | } 75 | 76 | // Globalize sails app as `server` 77 | global.server = app; 78 | 79 | // Globalize sails.io client as `io` 80 | global.io = io; 81 | return cb(err); 82 | }); 83 | 84 | }, 85 | 86 | 87 | 88 | 89 | 90 | teardown: function (done) { 91 | 92 | // If the socket never connected, don't worry about disconnecting 93 | // TODO: 94 | // cancel the connection attempt if one exists- 95 | // or better yet, extend `disconnect()` to do this 96 | if (!global.io || !io.socket || !io.socket.isConnected()) { 97 | return done(); 98 | } 99 | 100 | // Disconnect socket 101 | io.socket.disconnect(); 102 | setTimeout(function ensureDisconnect () { 103 | 104 | // Ensure socket is actually disconnected 105 | var isActuallyDisconnected = (io.socket.isConnected() === false); 106 | 107 | // Tear down sails server 108 | global.server.lower(function (){ 109 | 110 | // Delete globals (just in case-- shouldn't matter) 111 | delete global.server; 112 | delete global.io; 113 | return done(); 114 | }); 115 | 116 | }, 0); 117 | } 118 | }; 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /test/queue.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var assert = require('assert'); 6 | var _ = require('lodash'); 7 | var sailsIO = require('../sails.io.js'); 8 | var Sails = require('sails/lib/app'); 9 | var async = require('async'); 10 | 11 | // Use a weird port to avoid tests failing if we 12 | // forget to shut down another Sails app 13 | var TEST_SERVER_PORT = 1577; 14 | 15 | 16 | describe('before connecting, socket', function () { 17 | 18 | 19 | it('should allow requests to be queued, then execute their callbacks when the socket connects and is able to process the queue', function (done){ 20 | 21 | _buildSailsApp(function (err, app){ 22 | if (err) return done(err); 23 | 24 | app.get('/foo', function (req,res){ 25 | res.send('foo'); 26 | }); 27 | 28 | var io = _getFreshClient(); 29 | 30 | // 31 | // Notice we're not waiting for the connection to do this. 32 | // 33 | 34 | io.socket.get('/foo', function (body, jwr) { 35 | assert(body==='foo'); 36 | assert(jwr.statusCode===200); 37 | 38 | app.lower(function () { 39 | return done(); 40 | }); 41 | }); 42 | }); 43 | 44 | }); 45 | 46 | 47 | it('should allow comet listeners to be bound to the fake TmpSocket, then rebind them to the actual socket once it connects and is therefore capable of receiving events', function (done){ 48 | 49 | // Note that we're not actually QUEUING comet events that were sent 50 | // while the socket connects-- there's no way to do that client-side, 51 | // and it wouldn't really be that helpful. 52 | // 53 | // Instead, the purpose of this test is to make sure that the listeners 54 | // "make it" to the real socket once it does connect. 55 | 56 | _buildSailsApp(function (err, app){ 57 | if (err) return done(err); 58 | 59 | var io = _getFreshClient(); 60 | 61 | // 62 | // Notice we're not waiting for the connection to do this. 63 | // 64 | 65 | io.socket.on('foo', function (event){ 66 | assert('oh hi' === event); 67 | app.lower(function () { 68 | return done(); 69 | }); 70 | }); 71 | 72 | // After a while, emit an event on the app. 73 | setTimeout(function (){ 74 | app.sockets.blast('foo', 'oh hi'); 75 | }, 1500); 76 | 77 | }); 78 | 79 | }); 80 | 81 | 82 | 83 | 84 | it('should allow MULTIPLE comet listeners to be bound to the SAME EVENT before the socket has connected, then rebind them to the actual socket once it connects', function (done){ 85 | 86 | _buildSailsApp(function (err, app){ 87 | if (err) return done(err); 88 | 89 | var io = _getFreshClient(); 90 | 91 | // 92 | // Notice we're not waiting for the connection to do this. 93 | // 94 | 95 | var numCometEventsFired = 0; 96 | io.socket.on('foo', function (event){ 97 | assert('oh hi' === event); 98 | numCometEventsFired++; 99 | }); 100 | io.socket.on('foo', function (event){ 101 | assert('oh hi' === event); 102 | numCometEventsFired++; 103 | }); 104 | 105 | async.until(function _until(){ 106 | return numCometEventsFired===2; 107 | }, 108 | function _do(next) { 109 | setTimeout(next, 250); 110 | }, 111 | function _afterwards(err) { 112 | if (err) return done(err); 113 | app.lower(function () { 114 | return done(); 115 | }); 116 | }); 117 | 118 | // After a while, emit an event on the app. 119 | setTimeout(function (){ 120 | app.sockets.blast('foo', 'oh hi'); 121 | }, 1500); 122 | 123 | }); 124 | 125 | }); 126 | 127 | }); 128 | 129 | 130 | 131 | 132 | /** 133 | * @optional {String} url 134 | * @return {io} 135 | */ 136 | function _getFreshClient (url) { 137 | 138 | // Invalidate socket.io-client in require cache 139 | _.each(_.keys(require.cache), function (modulePath) { 140 | if (modulePath.match(/socket.io-client/)){ 141 | delete require.cache[modulePath]; 142 | } 143 | }); 144 | 145 | // Re-require the socket.io client (it's a singleton) 146 | var socketIOClient = require('socket.io-client'); 147 | 148 | // Instantiate a sails.io.js client and configure the url. 149 | var io = sailsIO(socketIOClient); 150 | io.sails.url = url||'http://localhost:'+TEST_SERVER_PORT; 151 | // Disable logger in sails.io.js client 152 | io.sails.environment = 'production'; 153 | 154 | return io; 155 | } 156 | 157 | 158 | 159 | function _buildSailsApp(cb) { 160 | 161 | // Set up a server to test against 162 | var app = new Sails(); 163 | app.lift({ 164 | port: TEST_SERVER_PORT, 165 | log: {level:'silent'}, 166 | hooks: {grunt: false} 167 | },function (err) { 168 | if (err) return cb(err); 169 | else cb(null, app); 170 | }); 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [icon of a life preserver - the emblem of the sails client SDK](https://github.com/balderdashy/sails.io.js) Sails JavaScript Client SDK 2 | 3 | [![Bower version](https://badge.fury.io/bo/sails.io.js.png)](http://badge.fury.io/bo/sails.io.js) 4 | [![NPM version](https://badge.fury.io/js/sails.io.js.png)](http://badge.fury.io/js/sails.io.js)     5 | [![Build Status](https://travis-ci.org/balderdashy/sails.io.js.svg?branch=master)](https://travis-ci.org/balderdashy/sails.io.js) 6 | 7 | JavaScript SDK for communicating w/ Sails via sockets from Node.js or the browser. 8 | 9 | ======================================== 10 | 11 | ### Contents 12 | 13 | | | Jump to... | 14 | |-----|-------------------------| 15 | | I | [Browser](https://github.com/balderdashy/sails.io.js#for-the-browser) | 16 | | II | [Node.js](https://github.com/balderdashy/sails.io.js#for-nodejs) | 17 | | III | [Version Notes](https://github.com/balderdashy/sails.io.js#version) | 18 | | IV | [License](https://github.com/balderdashy/sails.io.js#license) | 19 | 20 | ======================================== 21 | 22 | 23 | ### For the Browser 24 | 25 | #### Installation 26 | 27 | The `sails.io.js` client comes automatically installed in new Sails projects, but there is nothing _project-specific_ about the client SDK. You can just as easily copy and paste it yourself, get it from Bower, or just link a script tag directly to a hosted CDN. 28 | 29 | 47 | 48 | ###### Using Bower 49 | 50 | ```sh 51 | $ bower install sails.io.js 52 | ``` 53 | 54 | > Always use the version of `sails.io.js` that is compatible with your version of Sails. This repository (and Bower) will always include the version of `sails.io.js` that is compatible with the *latest* Sails version. If you have an older install, use the copy that is included in the `assets/js/dependencies` folder of your Sails app. 55 | 56 | #### Basic Usage 57 | 58 | ```html 59 | 60 | 61 | 62 | 85 | 86 | ``` 87 | 88 | #### Advanced Usage 89 | 90 | ###### Cross-domain 91 | 92 | Connect to a server other than the one that served ths project (i.e. on a different domain/subdomain): 93 | 94 | ```html 95 | 96 | 99 | ``` 100 | 101 | > Note that in order for `req.session` on a cross-domain server to work, there is a bit of pregaming that sails.io.js does behind the scenes. This is because it relies on cookies, and browsers (thankfully) do not let us access cross-origin cookies. 102 | > This JavaScript SDK circumvents that issue by (1) detecting a cross-origin scenario by examining `window.location` (if available) and comparing it with the connection base URL, then (2) sending a JSONP request to the cross-origin server in order to gain access to a cookie. 103 | > In order for that to work, the cross-origin sails server must have CORS enabled for `http://yourdomain.com` so that 3rd-party cookies are granted with the JSONP request. 104 | > Fortunately, Sails supports this behavior out of the box. 105 | > 106 | > For example, imagine the sails.io.js client is being used on a webpage served from a Sails server (or any other kind of server, like nginx) at `http://yourdomain.com`, but you need to connect a WebSocket to a different Sails server at `http://api.your-other-domain.com`. 107 | > First, sails.io.js will send a JSONP request to the configured "cookie route" (i.e. `/__getcookie` by default). That particular "cookie route" comes with CORS enabled out of the box, which means it will grant cookies to 3rd party domains. In your `config/sockets.js` file, you can restrict cross-domain cookie access to particular domains (i.e. `http://yourdomain.com`, in this example) 108 | 109 | 110 | 111 | 112 | ###### Disable `autoConnect` and/or connect sockets manually 113 | 114 | Disable `io.socket` and its auto-connecting behavior and/or connect 1 or more sockets manually: 115 | 116 | ```html 117 | 118 | 134 | ``` 135 | 136 | > Note that the `io.sails` config functions as the default for all connected sockets, but it can be overridden on a socket-by-socket basis by passing in an object to `io.sails.connect(opts)` 137 | 138 | ###### Change the `transports` used to connect to the server 139 | 140 | In some cases you may want to change the transorts that the socket client uses to connect to the server, and vice versa. For instance, some server environments--*notably Heroku*--do not support "sticky load balancing", causing the "polling" transport to fail. In these cases, you should first [change the transports listed in the `config/sockets.js` file](http://sailsjs.org/#/documentation/reference/sails.config/sails.config.sockets.html?q=advanced-configuration) in your Sails app. Then change the transports in the client by setting `io.sails.transports`: 141 | 142 | ```html 143 | 144 | 147 | ``` 148 | 149 | 150 | #### RequireJS/AMD Usage 151 | 152 | To use this in an AMD environment, *use the sails.io.js in the root* of this repo, not in dist. The dist build bundles a version of the socket.io client which will cause errors when trying to load two anonymous AMD modules from the same file. The file in root is not bundled with socket.io 153 | 154 | Usage with AMD will be very similar as node. Require in sails.io, socket.io, and instantiate the sails.io client: 155 | 156 | ```js 157 | define(['path/to/socketIOClient', 'path/to/sailsIOClient'], function(socketIOClient, sailsIOClient) { 158 | var io = sailsIOClient(socketIOClient); 159 | io.sails.url = 'http:/example.com'; 160 | 161 | io.socket.get('/example/path', { data: 'example' }, function(response) { 162 | console.log('got response', response) 163 | }); 164 | }); 165 | ``` 166 | 167 | 168 | 169 | ======================================== 170 | 171 | ### For Node.js 172 | 173 | > **Why would I use this from a Node script?** 174 | > 175 | > Most commonly, this SDK is useful on the backend when writing tests. However, any time you'd want to use a WebSocket or Socket.io client from Node to talk to a Sails server, you can use this module to allow for the ordinary usage you're familiar with in the browser-- namely using the socket interpreter to simulate HTTP over WebSockets. 176 | 177 | #### Installation 178 | 179 | ```sh 180 | $ npm install socket.io-client 181 | $ npm install sails.io.js 182 | ``` 183 | 184 | #### Basic Usage 185 | 186 | ```js 187 | var socketIOClient = require('socket.io-client'); 188 | var sailsIOClient = require('sails.io.js'); 189 | 190 | // Instantiate the socket client (`io`) 191 | // (for now, you must explicitly pass in the socket.io client when using this library from Node.js) 192 | var io = sailsIOClient(socketIOClient); 193 | 194 | // Set some options: 195 | // (you have to specify the host and port of the Sails backend when using this library from Node.js) 196 | io.sails.url = 'http://localhost:1337'; 197 | // ... 198 | 199 | // Send a GET request to `http://localhost:1337/hello`: 200 | io.socket.get('/hello', function serverResponded (body, JWR) { 201 | // body === JWR.body 202 | console.log('Sails responded with: ', body); 203 | console.log('with headers: ', JWR.headers); 204 | console.log('and with status code: ', JWR.statusCode); 205 | 206 | // When you are finished with `io.socket`, or any other sockets you connect manually, 207 | // you should make sure and disconnect them, e.g.: 208 | io.socket.disconnect(); 209 | 210 | // (note that there is no callback argument to the `.disconnect` method) 211 | }); 212 | 213 | ``` 214 | 215 | See the [tests in this repository](https://github.com/balderdashy/sails.io.js/blob/master/test/helpers/lifecycle.js) for more examples. 216 | 217 | ======================================== 218 | 219 | ### Version 220 | 221 | This repository holds the socket client SDK for Sails versions 0.11.0 and up. If you're looking for the SDK for the v0.9.x releases of Sails, the source is [located here](https://github.com/balderdashy/sails/blob/v0.9.16/bin/boilerplates/assets/js/sails.io.js). If you're looking for v0.10.x, check out the relevant [tags](https://github.com/balderdashy/sails.io.js/releases/tag/v0.10.3). 222 | 223 | ======================================== 224 | 225 | ### License 226 | 227 | **[MIT](./LICENSE)** 228 | © 2014- 229 | [Mike McNeil](http://michaelmcneil.com), [Balderdash](http://balderdash.co) & contributors 230 | 231 | This module is part of the [Sails framework](http://sailsjs.org), and is free and open-source under the [MIT License](http://sails.mit-license.org/). 232 | 233 | 234 | ![image_squidhome@2x.png](http://i.imgur.com/RIvu9.png) 235 | 236 | 237 | [![githalytics.com alpha](https://cruel-carlota.pagodabox.com/a22d3919de208c90c898986619efaa85 "githalytics.com")](http://githalytics.com/balderdashy/sails.io.js) 238 | -------------------------------------------------------------------------------- /sails.io.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sails.io.js 3 | * ------------------------------------------------------------------------ 4 | * JavaScript Client (SDK) for communicating with Sails. 5 | * 6 | * Note that this script is completely optional, but it is handy if you're 7 | * using WebSockets from the browser to talk to your Sails server. 8 | * 9 | * For tips and documentation, visit: 10 | * http://sailsjs.org/#!documentation/reference/BrowserSDK/BrowserSDK.html 11 | * ------------------------------------------------------------------------ 12 | * 13 | * This file allows you to send and receive socket.io messages to & from Sails 14 | * by simulating a REST client interface on top of socket.io. It models its API 15 | * after the $.ajax pattern from jQuery you might already be familiar with. 16 | * 17 | * So if you're switching from using AJAX to sockets, instead of: 18 | * `$.post( url, [data], [cb] )` 19 | * 20 | * You would use: 21 | * `socket.post( url, [data], [cb] )` 22 | */ 23 | 24 | 25 | (function() { 26 | 27 | // Save the URL that this script was fetched from for use below. 28 | // (skip this if this SDK is being used outside of the DOM, i.e. in a Node process) 29 | var urlThisScriptWasFetchedFrom = (function() { 30 | if ( 31 | typeof window !== 'object' || 32 | typeof window.document !== 'object' || 33 | typeof window.document.getElementsByTagName !== 'function' 34 | ) { 35 | return ''; 36 | } 37 | 38 | // Return the URL of the last script loaded (i.e. this one) 39 | // (this must run before nextTick; see http://stackoverflow.com/a/2976714/486547) 40 | var allScriptsCurrentlyInDOM = window.document.getElementsByTagName('script'); 41 | var thisScript = allScriptsCurrentlyInDOM[allScriptsCurrentlyInDOM.length - 1]; 42 | return thisScript.src; 43 | })(); 44 | 45 | // Constants 46 | var CONNECTION_METADATA_PARAMS = { 47 | version: '__sails_io_sdk_version', 48 | platform: '__sails_io_sdk_platform', 49 | language: '__sails_io_sdk_language' 50 | }; 51 | 52 | // Current version of this SDK (sailsDK?!?!) and other metadata 53 | // that will be sent along w/ the initial connection request. 54 | var SDK_INFO = { 55 | version: '0.11.0', // TODO: pull this automatically from package.json during build. 56 | platform: typeof module === 'undefined' ? 'browser' : 'node', 57 | language: 'javascript' 58 | }; 59 | SDK_INFO.versionString = 60 | CONNECTION_METADATA_PARAMS.version + '=' + SDK_INFO.version + '&' + 61 | CONNECTION_METADATA_PARAMS.platform + '=' + SDK_INFO.platform + '&' + 62 | CONNECTION_METADATA_PARAMS.language + '=' + SDK_INFO.language; 63 | 64 | 65 | // In case you're wrapping the socket.io client to prevent pollution of the 66 | // global namespace, you can pass in your own `io` to replace the global one. 67 | // But we still grab access to the global one if it's available here: 68 | var _io = (typeof io !== 'undefined') ? io : null; 69 | 70 | /** 71 | * Augment the `io` object passed in with methods for talking and listening 72 | * to one or more Sails backend(s). Automatically connects a socket and 73 | * exposes it on `io.socket`. If a socket tries to make requests before it 74 | * is connected, the sails.io.js client will queue it up. 75 | * 76 | * @param {SocketIO} io 77 | */ 78 | 79 | function SailsIOClient(io) { 80 | 81 | // Prefer the passed-in `io` instance, but also use the global one if we've got it. 82 | if (!io) { 83 | io = _io; 84 | } 85 | 86 | 87 | // If the socket.io client is not available, none of this will work. 88 | if (!io) throw new Error('`sails.io.js` requires a socket.io client, but `io` was not passed in.'); 89 | 90 | 91 | 92 | ////////////////////////////////////////////////////////////// 93 | ///// /////////////////////////// 94 | ///// PRIVATE METHODS/CONSTRUCTORS /////////////////////////// 95 | ///// /////////////////////////// 96 | ////////////////////////////////////////////////////////////// 97 | 98 | 99 | /** 100 | * A little logger for this library to use internally. 101 | * Basically just a wrapper around `console.log` with 102 | * support for feature-detection. 103 | * 104 | * @api private 105 | * @factory 106 | */ 107 | function LoggerFactory(options) { 108 | options = options || { 109 | prefix: true 110 | }; 111 | 112 | // If `console.log` is not accessible, `log` is a noop. 113 | if ( 114 | typeof console !== 'object' || 115 | typeof console.log !== 'function' || 116 | typeof console.log.bind !== 'function' 117 | ) { 118 | return function noop() {}; 119 | } 120 | 121 | return function log() { 122 | var args = Array.prototype.slice.call(arguments); 123 | 124 | // All logs are disabled when `io.sails.environment = 'production'`. 125 | if (io.sails.environment === 'production') return; 126 | 127 | // Add prefix to log messages (unless disabled) 128 | var PREFIX = ''; 129 | if (options.prefix) { 130 | args.unshift(PREFIX); 131 | } 132 | 133 | // Call wrapped logger 134 | console.log 135 | .bind(console) 136 | .apply(this, args); 137 | }; 138 | } 139 | 140 | // Create a private logger instance 141 | var consolog = LoggerFactory(); 142 | consolog.noPrefix = LoggerFactory({ 143 | prefix: false 144 | }); 145 | 146 | 147 | 148 | /** 149 | * What is the `requestQueue`? 150 | * 151 | * The request queue is used to simplify app-level connection logic-- 152 | * i.e. so you don't have to wait for the socket to be connected 153 | * to start trying to synchronize data. 154 | * 155 | * @api private 156 | * @param {SailsSocket} socket 157 | */ 158 | 159 | function runRequestQueue (socket) { 160 | var queue = socket.requestQueue; 161 | 162 | if (!queue) return; 163 | for (var i in queue) { 164 | 165 | // Double-check that `queue[i]` will not 166 | // inadvertently discover extra properties attached to the Object 167 | // and/or Array prototype by other libraries/frameworks/tools. 168 | // (e.g. Ember does this. See https://github.com/balderdashy/sails.io.js/pull/5) 169 | var isSafeToDereference = ({}).hasOwnProperty.call(queue, i); 170 | if (isSafeToDereference) { 171 | // Emit the request. 172 | _emitFrom(socket, queue[i]); 173 | } 174 | } 175 | 176 | // Now empty the queue to remove it as a source of additional complexity. 177 | queue = null; 178 | } 179 | 180 | 181 | 182 | /** 183 | * Send a JSONP request. 184 | * 185 | * @param {Object} opts [optional] 186 | * @param {Function} cb 187 | * @return {XMLHttpRequest} 188 | */ 189 | 190 | function jsonp(opts, cb) { 191 | opts = opts || {}; 192 | 193 | if (typeof window === 'undefined') { 194 | // TODO: refactor node usage to live in here 195 | return cb(); 196 | } 197 | 198 | var scriptEl = document.createElement('script'); 199 | window._sailsIoJSConnect = function(response) { 200 | scriptEl.parentNode.removeChild(scriptEl); 201 | 202 | cb(response); 203 | }; 204 | scriptEl.src = opts.url; 205 | document.getElementsByTagName('head')[0].appendChild(scriptEl); 206 | 207 | } 208 | 209 | 210 | 211 | /** 212 | * The JWR (JSON WebSocket Response) received from a Sails server. 213 | * 214 | * @api public 215 | * @param {Object} responseCtx 216 | * => :body 217 | * => :statusCode 218 | * => :headers 219 | * 220 | * @constructor 221 | */ 222 | 223 | function JWR(responseCtx) { 224 | this.body = responseCtx.body || {}; 225 | this.headers = responseCtx.headers || {}; 226 | this.statusCode = responseCtx.statusCode || 200; 227 | if (this.statusCode < 200 || this.statusCode >= 400) { 228 | this.error = this.body || this.statusCode; 229 | } 230 | } 231 | JWR.prototype.toString = function() { 232 | return '[ResponseFromSails]' + ' -- ' + 233 | 'Status: ' + this.statusCode + ' -- ' + 234 | 'Headers: ' + this.headers + ' -- ' + 235 | 'Body: ' + this.body; 236 | }; 237 | JWR.prototype.toPOJO = function() { 238 | return { 239 | body: this.body, 240 | headers: this.headers, 241 | statusCode: this.statusCode 242 | }; 243 | }; 244 | JWR.prototype.pipe = function() { 245 | // TODO: look at substack's stuff 246 | return new Error('Client-side streaming support not implemented yet.'); 247 | }; 248 | 249 | 250 | /** 251 | * @api private 252 | * @param {SailsSocket} socket [description] 253 | * @param {Object} requestCtx [description] 254 | */ 255 | 256 | function _emitFrom(socket, requestCtx) { 257 | 258 | if (!socket._raw) { 259 | throw new Error('Failed to emit from socket- raw SIO socket is missing.'); 260 | } 261 | 262 | // Since callback is embedded in requestCtx, 263 | // retrieve it and delete the key before continuing. 264 | var cb = requestCtx.cb; 265 | delete requestCtx.cb; 266 | 267 | // Name of the appropriate socket.io listener on the server 268 | // ( === the request method or "verb", e.g. 'get', 'post', 'put', etc. ) 269 | var sailsEndpoint = requestCtx.method; 270 | 271 | socket._raw.emit(sailsEndpoint, requestCtx, function serverResponded(responseCtx) { 272 | 273 | // Send back (emulatedHTTPBody, jsonWebSocketResponse) 274 | if (cb) { 275 | cb(responseCtx.body, new JWR(responseCtx)); 276 | } 277 | }); 278 | } 279 | 280 | ////////////////////////////////////////////////////////////// 281 | ///// //////////////////////// 282 | ////////////////////////////////////////////////////////////// 283 | 284 | 285 | 286 | // Version note: 287 | // 288 | // `io.SocketNamespace.prototype` doesn't exist in sio 1.0. 289 | // 290 | // Rather than adding methods to the prototype for the Socket instance that is returned 291 | // when the browser connects with `io.connect()`, we create our own constructor, `SailsSocket`. 292 | // This makes our solution more future-proof and helps us work better w/ the Socket.io team 293 | // when changes are rolled out in the future. To get a `SailsSocket`, you can run: 294 | // ``` 295 | // io.sails.connect(); 296 | // ``` 297 | 298 | 299 | 300 | /** 301 | * SailsSocket 302 | * 303 | * A wrapper for an underlying Socket instance that communicates directly 304 | * to the Socket.io server running inside of Sails. 305 | * 306 | * If no `socket` option is provied, SailsSocket will function as a mock. It will queue socket 307 | * requests and event handler bindings, replaying them when the raw underlying socket actually 308 | * connects. This is handy when we don't necessarily have the valid configuration to know 309 | * WHICH SERVER to talk to yet, etc. It is also used by `io.socket` for your convenience. 310 | * 311 | * @constructor 312 | */ 313 | 314 | function SailsSocket (opts){ 315 | var self = this; 316 | opts = opts||{}; 317 | 318 | // Absorb opts 319 | self.useCORSRouteToGetCookie = opts.useCORSRouteToGetCookie; 320 | self.url = opts.url; 321 | self.multiplex = opts.multiplex; 322 | self.transports = opts.transports; 323 | self.query = opts.query; 324 | 325 | // Set up "eventQueue" to hold event handlers which have not been set on the actual raw socket yet. 326 | self.eventQueue = {}; 327 | 328 | // Listen for special `parseError` event sent from sockets hook on the backend 329 | // if an error occurs but a valid callback was not received from the client 330 | // (i.e. so the server had no other way to send back the error information) 331 | self.on('sails:parseError', function (err){ 332 | consolog('Sails encountered an error parsing a socket message sent from this client, and did not have access to a callback function to respond with.'); 333 | consolog('Error details:',err); 334 | }); 335 | 336 | // TODO: 337 | // Listen for a special private message on any connected that allows the server 338 | // to set the environment (giving us 100% certainty that we guessed right) 339 | // However, note that the `console.log`s called before and after connection 340 | // are still forced to rely on our existing heuristics (to disable, tack #production 341 | // onto the URL used to fetch this file.) 342 | } 343 | 344 | 345 | /** 346 | * Start connecting this socket. 347 | * 348 | * @api private 349 | */ 350 | SailsSocket.prototype._connect = function (){ 351 | var self = this; 352 | 353 | // Apply `io.sails` config as defaults 354 | // (now that at least one tick has elapsed) 355 | self.useCORSRouteToGetCookie = self.useCORSRouteToGetCookie||io.sails.useCORSRouteToGetCookie; 356 | self.url = self.url||io.sails.url; 357 | self.transports = self.transports || io.sails.transports; 358 | self.query = self.query || io.sails.query; 359 | 360 | // Ensure URL has no trailing slash 361 | self.url = self.url ? self.url.replace(/(\/)$/, '') : undefined; 362 | 363 | // Mix the current SDK version into the query string in 364 | // the connection request to the server: 365 | if (typeof self.query !== 'string') self.query = SDK_INFO.versionString; 366 | else self.query += '&' + SDK_INFO.versionString; 367 | 368 | // Determine whether this is a cross-origin socket by examining the 369 | // hostname and port on the `window.location` object. 370 | var isXOrigin = (function (){ 371 | 372 | // If `window` doesn't exist (i.e. being used from node.js), then it's 373 | // always "cross-domain". 374 | if (typeof window === 'undefined' || typeof window.location === 'undefined') { 375 | return false; 376 | } 377 | 378 | // If `self.url` (aka "target") is falsy, then we don't need to worry about it. 379 | if (typeof self.url !== 'string') { return false; } 380 | 381 | // Get information about the "target" (`self.url`) 382 | var targetProtocol = (function (){ 383 | try { 384 | targetProtocol = self.url.match(/^([a-z]+:\/\/)/i)[1].toLowerCase(); 385 | } 386 | catch (e) {} 387 | targetProtocol = targetProtocol || 'http://'; 388 | return targetProtocol; 389 | })(); 390 | var isTargetSSL = !!self.url.match('^https'); 391 | var targetPort = (function (){ 392 | try { 393 | return self.url.match(/^[a-z]+:\/\/[^:]*:([0-9]*)/i)[1]; 394 | } 395 | catch (e){} 396 | return isTargetSSL ? '443' : '80'; 397 | })(); 398 | var targetAfterProtocol = self.url.replace(/^([a-z]+:\/\/)/i, ''); 399 | 400 | 401 | // If target protocol is different than the actual protocol, 402 | // then we'll consider this cross-origin. 403 | if (targetProtocol.replace(/[:\/]/g, '') !== window.location.protocol.replace(/[:\/]/g,'')) { 404 | return true; 405 | } 406 | 407 | 408 | // If target hostname is different than actual hostname, we'll consider this cross-origin. 409 | var hasSameHostname = targetAfterProtocol.search(window.location.hostname) === 0; 410 | if (!hasSameHostname) { 411 | return true; 412 | } 413 | 414 | // If no actual port is explicitly set on the `window.location` object, 415 | // we'll assume either 80 or 443. 416 | var isLocationSSL = window.location.protocol.match(/https/i); 417 | var locationPort = (window.location.port+'') || (isLocationSSL ? '443' : '80'); 418 | 419 | // Finally, if ports don't match, we'll consider this cross-origin. 420 | if (targetPort !== locationPort) { 421 | return true; 422 | } 423 | 424 | // Otherwise, it's the same origin. 425 | return false; 426 | 427 | })(); 428 | 429 | 430 | // Prepare to start connecting the socket 431 | (function selfInvoking (cb){ 432 | 433 | // If this is an attempt at a cross-origin or cross-port 434 | // socket connection, send a JSONP request first to ensure 435 | // that a valid cookie is available. This can be disabled 436 | // by setting `io.sails.useCORSRouteToGetCookie` to false. 437 | // 438 | // Otherwise, skip the stuff below. 439 | if (!(self.useCORSRouteToGetCookie && isXOrigin)) { 440 | return cb(); 441 | } 442 | 443 | // Figure out the x-origin CORS route 444 | // (Sails provides a default) 445 | var xOriginCookieURL = self.url; 446 | if (typeof self.useCORSRouteToGetCookie === 'string') { 447 | xOriginCookieURL += self.useCORSRouteToGetCookie; 448 | } 449 | else { 450 | xOriginCookieURL += '/__getcookie'; 451 | } 452 | 453 | 454 | // Make the AJAX request (CORS) 455 | if (typeof window !== 'undefined') { 456 | jsonp({ 457 | url: xOriginCookieURL, 458 | method: 'GET' 459 | }, cb); 460 | return; 461 | } 462 | 463 | // If there's no `window` object, we must be running in Node.js 464 | // so just require the request module and send the HTTP request that 465 | // way. 466 | var mikealsReq = require('request'); 467 | mikealsReq.get(xOriginCookieURL, function(err, httpResponse, body) { 468 | if (err) { 469 | consolog( 470 | 'Failed to connect socket (failed to get cookie)', 471 | 'Error:', err 472 | ); 473 | return; 474 | } 475 | cb(); 476 | }); 477 | 478 | })(function goAheadAndActuallyConnect() { 479 | 480 | // Now that we're ready to connect, create a raw underlying Socket 481 | // using Socket.io and save it as `_raw` (this will start it connecting) 482 | self._raw = io(self.url, self); 483 | 484 | // Replay event bindings from the eager socket 485 | self.replay(); 486 | 487 | 488 | /** 489 | * 'connect' event is triggered when the socket establishes a connection 490 | * successfully. 491 | */ 492 | self.on('connect', function socketConnected() { 493 | 494 | consolog.noPrefix( 495 | '\n' + 496 | '\n' + 497 | // ' |> ' + '\n' + 498 | // ' \\___/ '+️ 499 | // '\n'+ 500 | ' |> Now connected to Sails.' + '\n' + 501 | '\\___/ For help, see: http://bit.ly/1DmTvgK' + '\n' + 502 | ' (using '+io.sails.sdk.platform+' SDK @v'+io.sails.sdk.version+')'+ '\n' + 503 | '\n'+ 504 | '\n'+ 505 | // '\n'+ 506 | '' 507 | // ' ⚓︎ (development mode)' 508 | // 'e.g. to send a GET request to Sails via WebSockets, run:'+ '\n' + 509 | // '`io.socket.get("/foo", function serverRespondedWith (body, jwr) { console.log(body); })`'+ '\n' + 510 | ); 511 | }); 512 | 513 | self.on('disconnect', function() { 514 | self.connectionLostTimestamp = (new Date()).getTime(); 515 | consolog('===================================='); 516 | consolog('Socket was disconnected from Sails.'); 517 | consolog('Usually, this is due to one of the following reasons:' + '\n' + 518 | ' -> the server ' + (self.url ? self.url + ' ' : '') + 'was taken down' + '\n' + 519 | ' -> your browser lost internet connectivity'); 520 | consolog('===================================='); 521 | }); 522 | 523 | self.on('reconnecting', function(numAttempts) { 524 | consolog( 525 | '\n'+ 526 | ' Socket is trying to reconnect to Sails...\n'+ 527 | '_-|>_- (attempt #' + numAttempts + ')'+'\n'+ 528 | '\n' 529 | ); 530 | }); 531 | 532 | self.on('reconnect', function(transport, numAttempts) { 533 | var msSinceConnectionLost = ((new Date()).getTime() - self.connectionLostTimestamp); 534 | var numSecsOffline = (msSinceConnectionLost / 1000); 535 | consolog( 536 | '\n'+ 537 | ' |> Socket reconnected successfully after'+'\n'+ 538 | '\\___/ being offline for ~' + numSecsOffline + ' seconds.'+'\n'+ 539 | '\n' 540 | ); 541 | }); 542 | 543 | // 'error' event is triggered if connection can not be established. 544 | // (usually because of a failed authorization, which is in turn 545 | // usually due to a missing or invalid cookie) 546 | self.on('error', function failedToConnect(err) { 547 | 548 | // TODO: 549 | // handle failed connections due to failed authorization 550 | // in a smarter way (probably can listen for a different event) 551 | 552 | // A bug in Socket.io 0.9.x causes `connect_failed` 553 | // and `reconnect_failed` not to fire. 554 | // Check out the discussion in github issues for details: 555 | // https://github.com/LearnBoost/socket.io/issues/652 556 | // io.socket.on('connect_failed', function () { 557 | // consolog('io.socket emitted `connect_failed`'); 558 | // }); 559 | // io.socket.on('reconnect_failed', function () { 560 | // consolog('io.socket emitted `reconnect_failed`'); 561 | // }); 562 | 563 | consolog( 564 | 'Failed to connect socket (probably due to failed authorization on server)', 565 | 'Error:', err 566 | ); 567 | }); 568 | }); 569 | 570 | }; 571 | 572 | 573 | /** 574 | * Disconnect the underlying socket. 575 | * 576 | * @api public 577 | */ 578 | SailsSocket.prototype.disconnect = function (){ 579 | if (!this._raw) { 580 | throw new Error('Cannot disconnect- socket is already disconnected'); 581 | } 582 | return this._raw.disconnect(); 583 | }; 584 | 585 | 586 | 587 | /** 588 | * isConnected 589 | * 590 | * @api private 591 | * @return {Boolean} whether the socket is connected and able to 592 | * communicate w/ the server. 593 | */ 594 | 595 | SailsSocket.prototype.isConnected = function () { 596 | if (!this._raw) { 597 | return false; 598 | } 599 | 600 | return !!this._raw.connected; 601 | }; 602 | 603 | 604 | 605 | /** 606 | * [replay description] 607 | * @return {[type]} [description] 608 | */ 609 | SailsSocket.prototype.replay = function (){ 610 | var self = this; 611 | 612 | // Pass events and a reference to the request queue 613 | // off to the self._raw for consumption 614 | for (var evName in self.eventQueue) { 615 | for (var i in self.eventQueue[evName]) { 616 | self._raw.on(evName, self.eventQueue[evName][i]); 617 | } 618 | } 619 | 620 | // Bind a one-time function to run the request queue 621 | // when the self._raw connects. 622 | if ( !self.isConnected() ) { 623 | var alreadyRanRequestQueue = false; 624 | self._raw.on('connect', function whenRawSocketConnects() { 625 | if (alreadyRanRequestQueue) return; 626 | runRequestQueue(self); 627 | alreadyRanRequestQueue = true; 628 | }); 629 | } 630 | // Or run it immediately if self._raw is already connected 631 | else { 632 | runRequestQueue(self); 633 | } 634 | 635 | return self; 636 | }; 637 | 638 | 639 | /** 640 | * Chainable method to bind an event to the socket. 641 | * 642 | * @param {String} evName [event name] 643 | * @param {Function} fn [event handler function] 644 | * @return {SailsSocket} 645 | */ 646 | SailsSocket.prototype.on = function (evName, fn){ 647 | 648 | // Bind the event to the raw underlying socket if possible. 649 | if (this._raw) { 650 | this._raw.on(evName, fn); 651 | return this; 652 | } 653 | 654 | // Otherwise queue the event binding. 655 | if (!this.eventQueue[evName]) { 656 | this.eventQueue[evName] = [fn]; 657 | } 658 | else { 659 | this.eventQueue[evName].push(fn); 660 | } 661 | 662 | return this; 663 | }; 664 | 665 | /** 666 | * Chainable method to unbind an event from the socket. 667 | * 668 | * @param {String} evName [event name] 669 | * @param {Function} fn [event handler function] 670 | * @return {SailsSocket} 671 | */ 672 | SailsSocket.prototype.off = function (evName, fn){ 673 | 674 | // Bind the event to the raw underlying socket if possible. 675 | if (this._raw) { 676 | this._raw.off(evName, fn); 677 | return this; 678 | } 679 | 680 | // Otherwise queue the event binding. 681 | if (this.eventQueue[evName] && this.eventQueue[evName].indexOf(fn) > -1) { 682 | this.eventQueue[evName].splice(this.eventQueue[evName].indexOf(fn), 1); 683 | } 684 | 685 | return this; 686 | }; 687 | 688 | 689 | /** 690 | * Chainable method to unbind all events from the socket. 691 | * 692 | * @return {SailsSocket} 693 | */ 694 | SailsSocket.prototype.removeAllListeners = function (){ 695 | 696 | // Bind the event to the raw underlying socket if possible. 697 | if (this._raw) { 698 | this._raw.removeAllListeners(); 699 | return this; 700 | } 701 | 702 | // Otherwise queue the event binding. 703 | this.eventQueue = {}; 704 | 705 | return this; 706 | }; 707 | 708 | /** 709 | * Simulate a GET request to sails 710 | * e.g. 711 | * `socket.get('/user/3', Stats.populate)` 712 | * 713 | * @api public 714 | * @param {String} url :: destination URL 715 | * @param {Object} params :: parameters to send with the request [optional] 716 | * @param {Function} cb :: callback function to call when finished [optional] 717 | */ 718 | 719 | SailsSocket.prototype.get = function(url, data, cb) { 720 | 721 | // `data` is optional 722 | if (typeof data === 'function') { 723 | cb = data; 724 | data = {}; 725 | } 726 | 727 | return this.request({ 728 | method: 'get', 729 | params: data, 730 | url: url 731 | }, cb); 732 | }; 733 | 734 | 735 | 736 | /** 737 | * Simulate a POST request to sails 738 | * e.g. 739 | * `socket.post('/event', newMeeting, $spinner.hide)` 740 | * 741 | * @api public 742 | * @param {String} url :: destination URL 743 | * @param {Object} params :: parameters to send with the request [optional] 744 | * @param {Function} cb :: callback function to call when finished [optional] 745 | */ 746 | 747 | SailsSocket.prototype.post = function(url, data, cb) { 748 | 749 | // `data` is optional 750 | if (typeof data === 'function') { 751 | cb = data; 752 | data = {}; 753 | } 754 | 755 | return this.request({ 756 | method: 'post', 757 | data: data, 758 | url: url 759 | }, cb); 760 | }; 761 | 762 | 763 | 764 | /** 765 | * Simulate a PUT request to sails 766 | * e.g. 767 | * `socket.post('/event/3', changedFields, $spinner.hide)` 768 | * 769 | * @api public 770 | * @param {String} url :: destination URL 771 | * @param {Object} params :: parameters to send with the request [optional] 772 | * @param {Function} cb :: callback function to call when finished [optional] 773 | */ 774 | 775 | SailsSocket.prototype.put = function(url, data, cb) { 776 | 777 | // `data` is optional 778 | if (typeof data === 'function') { 779 | cb = data; 780 | data = {}; 781 | } 782 | 783 | return this.request({ 784 | method: 'put', 785 | params: data, 786 | url: url 787 | }, cb); 788 | }; 789 | 790 | 791 | 792 | /** 793 | * Simulate a DELETE request to sails 794 | * e.g. 795 | * `socket.delete('/event', $spinner.hide)` 796 | * 797 | * @api public 798 | * @param {String} url :: destination URL 799 | * @param {Object} params :: parameters to send with the request [optional] 800 | * @param {Function} cb :: callback function to call when finished [optional] 801 | */ 802 | 803 | SailsSocket.prototype['delete'] = function(url, data, cb) { 804 | 805 | // `data` is optional 806 | if (typeof data === 'function') { 807 | cb = data; 808 | data = {}; 809 | } 810 | 811 | return this.request({ 812 | method: 'delete', 813 | params: data, 814 | url: url 815 | }, cb); 816 | }; 817 | 818 | 819 | 820 | /** 821 | * Simulate an HTTP request to sails 822 | * e.g. 823 | * ``` 824 | * socket.request({ 825 | * url:'/user', 826 | * params: {}, 827 | * method: 'POST', 828 | * headers: {} 829 | * }, function (responseBody, JWR) { 830 | * // ... 831 | * }); 832 | * ``` 833 | * 834 | * @api public 835 | * @option {String} url :: destination URL 836 | * @option {Object} params :: parameters to send with the request [optional] 837 | * @option {Object} headers:: headers to send with the request [optional] 838 | * @option {Function} cb :: callback function to call when finished [optional] 839 | * @option {String} method :: HTTP request method [optional] 840 | */ 841 | 842 | SailsSocket.prototype.request = function(options, cb) { 843 | 844 | var usage = 845 | 'Usage:\n'+ 846 | 'socket.request( options, [fnToCallWhenComplete] )\n\n'+ 847 | 'options.url :: e.g. "/foo/bar"'+'\n'+ 848 | 'options.method :: e.g. "get", "post", "put", or "delete", etc.'+'\n'+ 849 | 'options.params :: e.g. { emailAddress: "mike@sailsjs.org" }'+'\n'+ 850 | 'options.headers :: e.g. { "x-my-custom-header": "some string" }'; 851 | // Old usage: 852 | // var usage = 'Usage:\n socket.'+(options.method||'request')+'('+ 853 | // ' destinationURL, [dataToSend], [fnToCallWhenComplete] )'; 854 | 855 | 856 | // Validate options and callback 857 | if (typeof options !== 'object' || typeof options.url !== 'string') { 858 | throw new Error('Invalid or missing URL!\n' + usage); 859 | } 860 | if (options.method && typeof options.method !== 'string') { 861 | throw new Error('Invalid `method` provided (should be a string like "post" or "put")\n' + usage); 862 | } 863 | if (options.headers && typeof options.headers !== 'object') { 864 | throw new Error('Invalid `headers` provided (should be an object with string values)\n' + usage); 865 | } 866 | if (options.params && typeof options.params !== 'object') { 867 | throw new Error('Invalid `params` provided (should be an object with string values)\n' + usage); 868 | } 869 | if (cb && typeof cb !== 'function') { 870 | throw new Error('Invalid callback function!\n' + usage); 871 | } 872 | 873 | 874 | // Build a simulated request object 875 | // (and sanitize/marshal options along the way) 876 | var requestCtx = { 877 | 878 | method: options.method.toLowerCase() || 'get', 879 | 880 | headers: options.headers || {}, 881 | 882 | data: options.params || options.data || {}, 883 | 884 | // Remove trailing slashes and spaces to make packets smaller. 885 | url: options.url.replace(/^(.+)\/*\s*$/, '$1'), 886 | 887 | cb: cb 888 | }; 889 | 890 | // If this socket is not connected yet, queue up this request 891 | // instead of sending it. 892 | // (so it can be replayed when the socket comes online.) 893 | if ( ! this.isConnected() ) { 894 | 895 | // If no queue array exists for this socket yet, create it. 896 | this.requestQueue = this.requestQueue || []; 897 | this.requestQueue.push(requestCtx); 898 | return; 899 | } 900 | 901 | 902 | // Otherwise, our socket is ok! 903 | // Send the request. 904 | _emitFrom(this, requestCtx); 905 | }; 906 | 907 | 908 | 909 | /** 910 | * Socket.prototype._request 911 | * 912 | * Simulate HTTP over Socket.io. 913 | * 914 | * @api private 915 | * @param {[type]} options [description] 916 | * @param {Function} cb [description] 917 | */ 918 | SailsSocket.prototype._request = function(options, cb) { 919 | throw new Error('`_request()` was a private API deprecated as of v0.11 of the sails.io.js client. Use `.request()` instead.'); 920 | }; 921 | 922 | 923 | 924 | // Set a `sails` object that may be used for configuration before the 925 | // first socket connects (i.e. to prevent auto-connect) 926 | io.sails = { 927 | 928 | // Whether to automatically connect a socket and save it as `io.socket`. 929 | autoConnect: true, 930 | 931 | // The route (path) to hit to get a x-origin (CORS) cookie 932 | // (or true to use the default: '/__getcookie') 933 | useCORSRouteToGetCookie: true, 934 | 935 | // The environment we're running in. 936 | // (logs are not displayed when this is set to 'production') 937 | // 938 | // Defaults to development unless this script was fetched from a URL 939 | // that ends in `*.min.js` or '#production' (may also be manually overridden.) 940 | // 941 | environment: urlThisScriptWasFetchedFrom.match(/(\#production|\.min\.js)/g) ? 'production' : 'development', 942 | 943 | // The version of this sails.io.js client SDK 944 | sdk: SDK_INFO, 945 | 946 | // Transports to use when communicating with the server, in the order they will be tried 947 | transports: ['polling', 'websocket'] 948 | }; 949 | 950 | 951 | 952 | /** 953 | * Add `io.sails.connect` function as a wrapper for the built-in `io()` aka `io.connect()` 954 | * method, returning a SailsSocket. This special function respects the configured io.sails 955 | * connection URL, as well as sending other identifying information (most importantly, the 956 | * current version of this SDK). 957 | * 958 | * @param {String} url [optional] 959 | * @param {Object} opts [optional] 960 | * @return {Socket} 961 | */ 962 | io.sails.connect = function(url, opts) { 963 | opts = opts || {}; 964 | 965 | // If explicit connection url is specified, save it to options 966 | opts.url = url || opts.url || undefined; 967 | 968 | // Instantiate and return a new SailsSocket- and try to connect immediately. 969 | var socket = new SailsSocket(opts); 970 | socket._connect(); 971 | return socket; 972 | }; 973 | 974 | 975 | 976 | // io.socket 977 | // 978 | // The eager instance of Socket which will automatically try to connect 979 | // using the host that this js file was served from. 980 | // 981 | // This can be disabled or configured by setting properties on `io.sails.*` within the 982 | // first cycle of the event loop. 983 | // 984 | 985 | 986 | // Build `io.socket` so it exists 987 | // (this does not start the connection process) 988 | io.socket = new SailsSocket(); 989 | 990 | // In the mean time, this eager socket will be queue events bound by the user 991 | // before the first cycle of the event loop (using `.on()`), which will later 992 | // be rebound on the raw underlying socket. 993 | 994 | // If configured to do so, start auto-connecting after the first cycle of the event loop 995 | // has completed (to allow time for this behavior to be configured/disabled 996 | // by specifying properties on `io.sails`) 997 | setTimeout(function() { 998 | 999 | // If autoConnect is disabled, delete the eager socket (io.socket) and bail out. 1000 | if (!io.sails.autoConnect) { 1001 | delete io.socket; 1002 | return; 1003 | } 1004 | 1005 | // consolog('Eagerly auto-connecting socket to Sails... (requests will be queued in the mean-time)'); 1006 | io.socket._connect(); 1007 | 1008 | 1009 | }, 0); // 1010 | 1011 | 1012 | // Return the `io` object. 1013 | return io; 1014 | } 1015 | 1016 | 1017 | // Add CommonJS support to allow this client SDK to be used from Node.js. 1018 | if (typeof module === 'object' && typeof module.exports !== 'undefined') { 1019 | module.exports = SailsIOClient; 1020 | return SailsIOClient; 1021 | } 1022 | else if (typeof define === 'function' && define.amd) { 1023 | // AMD. Register as an anonymous module. 1024 | define([], function() { 1025 | return SailsIOClient; 1026 | }); 1027 | } 1028 | else { 1029 | // Otherwise, try to instantiate the client: 1030 | // In case you're wrapping the socket.io client to prevent pollution of the 1031 | // global namespace, you can replace the global `io` with your own `io` here: 1032 | return SailsIOClient(); 1033 | } 1034 | 1035 | })(); 1036 | -------------------------------------------------------------------------------- /dependencies/socket.io.min.js: -------------------------------------------------------------------------------- 1 | // socket.io-1.2.1 2 | // (from http://socket.io/download/) 3 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.io=e()}}(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&!this.encoding){var pack=this.packetBuffer.shift();this.packet(pack)}};Manager.prototype.cleanup=function(){var sub;while(sub=this.subs.shift())sub.destroy();this.packetBuffer=[];this.encoding=false;this.decoder.destroy()};Manager.prototype.close=Manager.prototype.disconnect=function(){this.skipReconnect=true;this.readyState="closed";this.engine&&this.engine.close()};Manager.prototype.onclose=function(reason){debug("close");this.cleanup();this.readyState="closed";this.emit("close",reason);if(this._reconnection&&!this.skipReconnect){this.reconnect()}};Manager.prototype.reconnect=function(){if(this.reconnecting||this.skipReconnect)return this;var self=this;this.attempts++;if(this.attempts>this._reconnectionAttempts){debug("reconnect failed");this.emitAll("reconnect_failed");this.reconnecting=false}else{var delay=this.attempts*this.reconnectionDelay();delay=Math.min(delay,this.reconnectionDelayMax());debug("will wait %dms before reconnect attempt",delay);this.reconnecting=true;var timer=setTimeout(function(){if(self.skipReconnect)return;debug("attempting reconnect");self.emitAll("reconnect_attempt",self.attempts);self.emitAll("reconnecting",self.attempts);if(self.skipReconnect)return;self.open(function(err){if(err){debug("reconnect attempt error");self.reconnecting=false;self.reconnect();self.emitAll("reconnect_error",err.data)}else{debug("reconnect success");self.onreconnect()}})},delay);this.subs.push({destroy:function(){clearTimeout(timer)}})}};Manager.prototype.onreconnect=function(){var attempt=this.attempts;this.attempts=0;this.reconnecting=false;this.emitAll("reconnect",attempt)}},{"./on":4,"./socket":5,"./url":6,"component-bind":7,"component-emitter":8,debug:9,"engine.io-client":10,indexof:39,"object-component":40,"socket.io-parser":43}],4:[function(_dereq_,module,exports){module.exports=on;function on(obj,ev,fn){obj.on(ev,fn);return{destroy:function(){obj.removeListener(ev,fn)}}}},{}],5:[function(_dereq_,module,exports){var parser=_dereq_("socket.io-parser");var Emitter=_dereq_("component-emitter");var toArray=_dereq_("to-array");var on=_dereq_("./on");var bind=_dereq_("component-bind");var debug=_dereq_("debug")("socket.io-client:socket");var hasBin=_dereq_("has-binary");module.exports=exports=Socket;var events={connect:1,connect_error:1,connect_timeout:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1};var emit=Emitter.prototype.emit;function Socket(io,nsp){this.io=io;this.nsp=nsp;this.json=this;this.ids=0;this.acks={};if(this.io.autoConnect)this.open();this.receiveBuffer=[];this.sendBuffer=[];this.connected=false;this.disconnected=true}Emitter(Socket.prototype);Socket.prototype.subEvents=function(){if(this.subs)return;var io=this.io;this.subs=[on(io,"open",bind(this,"onopen")),on(io,"packet",bind(this,"onpacket")),on(io,"close",bind(this,"onclose"))]};Socket.prototype.open=Socket.prototype.connect=function(){if(this.connected)return this;this.subEvents();this.io.open();if("open"==this.io.readyState)this.onopen();return this};Socket.prototype.send=function(){var args=toArray(arguments);args.unshift("message");this.emit.apply(this,args);return this};Socket.prototype.emit=function(ev){if(events.hasOwnProperty(ev)){emit.apply(this,arguments);return this}var args=toArray(arguments);var parserType=parser.EVENT;if(hasBin(args)){parserType=parser.BINARY_EVENT}var packet={type:parserType,data:args};if("function"==typeof args[args.length-1]){debug("emitting packet with ack id %d",this.ids);this.acks[this.ids]=args.pop();packet.id=this.ids++}if(this.connected){this.packet(packet)}else{this.sendBuffer.push(packet)}return this};Socket.prototype.packet=function(packet){packet.nsp=this.nsp;this.io.packet(packet)};Socket.prototype.onopen=function(){debug("transport is open - connecting");if("/"!=this.nsp){this.packet({type:parser.CONNECT})}};Socket.prototype.onclose=function(reason){debug("close (%s)",reason);this.connected=false;this.disconnected=true;this.emit("disconnect",reason)};Socket.prototype.onpacket=function(packet){if(packet.nsp!=this.nsp)return;switch(packet.type){case parser.CONNECT:this.onconnect();break;case parser.EVENT:this.onevent(packet);break;case parser.BINARY_EVENT:this.onevent(packet);break;case parser.ACK:this.onack(packet);break;case parser.BINARY_ACK:this.onack(packet);break;case parser.DISCONNECT:this.ondisconnect();break;case parser.ERROR:this.emit("error",packet.data);break}};Socket.prototype.onevent=function(packet){var args=packet.data||[];debug("emitting event %j",args);if(null!=packet.id){debug("attaching ack callback to event");args.push(this.ack(packet.id))}if(this.connected){emit.apply(this,args)}else{this.receiveBuffer.push(args)}};Socket.prototype.ack=function(id){var self=this;var sent=false;return function(){if(sent)return;sent=true;var args=toArray(arguments);debug("sending ack %j",args);var type=hasBin(args)?parser.BINARY_ACK:parser.ACK;self.packet({type:type,id:id,data:args})}};Socket.prototype.onack=function(packet){debug("calling ack %s with %j",packet.id,packet.data);var fn=this.acks[packet.id];fn.apply(this,packet.data);delete this.acks[packet.id]};Socket.prototype.onconnect=function(){this.connected=true;this.disconnected=false;this.emit("connect");this.emitBuffered()};Socket.prototype.emitBuffered=function(){var i;for(i=0;i=hour)return(ms/hour).toFixed(1)+"h";if(ms>=min)return(ms/min).toFixed(1)+"m";if(ms>=sec)return(ms/sec|0)+"s";return ms+"ms"};debug.enabled=function(name){for(var i=0,len=debug.skips.length;i';iframe=document.createElement(html)}catch(e){iframe=document.createElement("iframe");iframe.name=self.iframeId;iframe.src="javascript:0"}iframe.id=self.iframeId;self.form.appendChild(iframe);self.iframe=iframe}initIframe();data=data.replace(rEscapedNewline,"\\\n");this.area.value=data.replace(rNewline,"\\n");try{this.form.submit()}catch(e){}if(this.iframe.attachEvent){this.iframe.onreadystatechange=function(){if(self.iframe.readyState=="complete"){complete()}}}else{this.iframe.onload=complete}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-inherit":20}],16:[function(_dereq_,module,exports){(function(global){var XMLHttpRequest=_dereq_("xmlhttprequest");var Polling=_dereq_("./polling");var Emitter=_dereq_("component-emitter");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling-xhr");module.exports=XHR;module.exports.Request=Request;function empty(){}function XHR(opts){Polling.call(this,opts);if(global.location){var isSSL="https:"==location.protocol;var port=location.port;if(!port){port=isSSL?443:80}this.xd=opts.hostname!=global.location.hostname||port!=opts.port;this.xs=opts.secure!=isSSL}}inherit(XHR,Polling);XHR.prototype.supportsBinary=true;XHR.prototype.request=function(opts){opts=opts||{};opts.uri=this.uri();opts.xd=this.xd;opts.xs=this.xs;opts.agent=this.agent||false;opts.supportsBinary=this.supportsBinary;opts.enablesXDR=this.enablesXDR;return new Request(opts)};XHR.prototype.doWrite=function(data,fn){var isBinary=typeof data!=="string"&&data!==undefined;var req=this.request({method:"POST",data:data,isBinary:isBinary});var self=this;req.on("success",fn);req.on("error",function(err){self.onError("xhr post error",err)});this.sendXhr=req};XHR.prototype.doPoll=function(){debug("xhr poll");var req=this.request();var self=this;req.on("data",function(data){self.onData(data)});req.on("error",function(err){self.onError("xhr poll error",err)});this.pollXhr=req};function Request(opts){this.method=opts.method||"GET";this.uri=opts.uri;this.xd=!!opts.xd;this.xs=!!opts.xs;this.async=false!==opts.async;this.data=undefined!=opts.data?opts.data:null;this.agent=opts.agent;this.isBinary=opts.isBinary;this.supportsBinary=opts.supportsBinary;this.enablesXDR=opts.enablesXDR;this.create()}Emitter(Request.prototype);Request.prototype.create=function(){var xhr=this.xhr=new XMLHttpRequest({agent:this.agent,xdomain:this.xd,xscheme:this.xs,enablesXDR:this.enablesXDR});var self=this;try{debug("xhr open %s: %s",this.method,this.uri);xhr.open(this.method,this.uri,this.async);if(this.supportsBinary){xhr.responseType="arraybuffer"}if("POST"==this.method){try{if(this.isBinary){xhr.setRequestHeader("Content-type","application/octet-stream")}else{xhr.setRequestHeader("Content-type","text/plain;charset=UTF-8")}}catch(e){}}if("withCredentials"in xhr){xhr.withCredentials=true}if(this.hasXDR()){xhr.onload=function(){self.onLoad()};xhr.onerror=function(){self.onError(xhr.responseText)}}else{xhr.onreadystatechange=function(){if(4!=xhr.readyState)return;if(200==xhr.status||1223==xhr.status){self.onLoad()}else{setTimeout(function(){self.onError(xhr.status)},0)}}}debug("xhr data %s",this.data);xhr.send(this.data)}catch(e){setTimeout(function(){self.onError(e)},0);return}if(global.document){this.index=Request.requestsCount++;Request.requests[this.index]=this}};Request.prototype.onSuccess=function(){this.emit("success");this.cleanup()};Request.prototype.onData=function(data){this.emit("data",data);this.onSuccess()};Request.prototype.onError=function(err){this.emit("error",err);this.cleanup()};Request.prototype.cleanup=function(){if("undefined"==typeof this.xhr||null===this.xhr){return}if(this.hasXDR()){this.xhr.onload=this.xhr.onerror=empty}else{this.xhr.onreadystatechange=empty}try{this.xhr.abort()}catch(e){}if(global.document){delete Request.requests[this.index]}this.xhr=null};Request.prototype.onLoad=function(){var data;try{var contentType;try{contentType=this.xhr.getResponseHeader("Content-Type").split(";")[0]}catch(e){}if(contentType==="application/octet-stream"){data=this.xhr.response}else{if(!this.supportsBinary){data=this.xhr.responseText}else{data="ok"}}}catch(e){this.onError(e)}if(null!=data){this.onData(data)}};Request.prototype.hasXDR=function(){return"undefined"!==typeof global.XDomainRequest&&!this.xs&&this.enablesXDR};Request.prototype.abort=function(){this.cleanup()};if(global.document){Request.requestsCount=0;Request.requests={};if(global.attachEvent){global.attachEvent("onunload",unloadHandler)}else if(global.addEventListener){global.addEventListener("beforeunload",unloadHandler,false)}}function unloadHandler(){for(var i in Request.requests){if(Request.requests.hasOwnProperty(i)){Request.requests[i].abort()}}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-emitter":8,"component-inherit":20,debug:21,xmlhttprequest:19}],17:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parseqs=_dereq_("parseqs");var parser=_dereq_("engine.io-parser");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling");module.exports=Polling;var hasXHR2=function(){var XMLHttpRequest=_dereq_("xmlhttprequest");var xhr=new XMLHttpRequest({xdomain:false});return null!=xhr.responseType}();function Polling(opts){var forceBase64=opts&&opts.forceBase64;if(!hasXHR2||forceBase64){this.supportsBinary=false}Transport.call(this,opts)}inherit(Polling,Transport);Polling.prototype.name="polling";Polling.prototype.doOpen=function(){this.poll()};Polling.prototype.pause=function(onPause){var pending=0;var self=this;this.readyState="pausing";function pause(){debug("paused");self.readyState="paused";onPause()}if(this.polling||!this.writable){var total=0;if(this.polling){debug("we are currently polling - waiting to pause");total++;this.once("pollComplete",function(){debug("pre-pause polling complete");--total||pause()})}if(!this.writable){debug("we are currently writing - waiting to pause");total++;this.once("drain",function(){debug("pre-pause writing complete");--total||pause()})}}else{pause()}};Polling.prototype.poll=function(){debug("polling");this.polling=true;this.doPoll();this.emit("poll")};Polling.prototype.onData=function(data){var self=this;debug("polling got data %s",data);var callback=function(packet,index,total){if("opening"==self.readyState){self.onOpen()}if("close"==packet.type){self.onClose();return false}self.onPacket(packet)};parser.decodePayload(data,this.socket.binaryType,callback);if("closed"!=this.readyState){this.polling=false;this.emit("pollComplete");if("open"==this.readyState){this.poll()}else{debug('ignoring poll - transport state "%s"',this.readyState)}}};Polling.prototype.doClose=function(){var self=this;function close(){debug("writing close packet");self.write([{type:"close"}])}if("open"==this.readyState){debug("transport open - closing");close()}else{debug("transport not open - deferring close");this.once("open",close)}};Polling.prototype.write=function(packets){var self=this;this.writable=false;var callbackfn=function(){self.writable=true;self.emit("drain")};var self=this;parser.encodePayload(packets,this.supportsBinary,function(data){self.doWrite(data,callbackfn)})};Polling.prototype.uri=function(){var query=this.query||{};var schema=this.secure?"https":"http";var port="";if(false!==this.timestampRequests){query[this.timestampParam]=+new Date+"-"+Transport.timestamps++}if(!this.supportsBinary&&!query.sid){query.b64=1}query=parseqs.encode(query);if(this.port&&("https"==schema&&this.port!=443||"http"==schema&&this.port!=80)){port=":"+this.port}if(query.length){query="?"+query}return schema+"://"+this.hostname+port+this.path+query}},{"../transport":13,"component-inherit":20,debug:21,"engine.io-parser":24,parseqs:32,xmlhttprequest:19}],18:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parser=_dereq_("engine.io-parser");var parseqs=_dereq_("parseqs");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:websocket");var WebSocket=_dereq_("ws");module.exports=WS;function WS(opts){var forceBase64=opts&&opts.forceBase64;if(forceBase64){this.supportsBinary=false}Transport.call(this,opts)}inherit(WS,Transport);WS.prototype.name="websocket";WS.prototype.supportsBinary=true;WS.prototype.doOpen=function(){if(!this.check()){return}var self=this;var uri=this.uri();var protocols=void 0;var opts={agent:this.agent};this.ws=new WebSocket(uri,protocols,opts);if(this.ws.binaryType===undefined){this.supportsBinary=false}this.ws.binaryType="arraybuffer";this.addEventListeners()};WS.prototype.addEventListeners=function(){var self=this;this.ws.onopen=function(){self.onOpen()};this.ws.onclose=function(){self.onClose()};this.ws.onmessage=function(ev){self.onData(ev.data)};this.ws.onerror=function(e){self.onError("websocket error",e)}};if("undefined"!=typeof navigator&&/iPad|iPhone|iPod/i.test(navigator.userAgent)){WS.prototype.onData=function(data){var self=this;setTimeout(function(){Transport.prototype.onData.call(self,data)},0)}}WS.prototype.write=function(packets){var self=this;this.writable=false;for(var i=0,l=packets.length;i=31}exports.formatters.j=function(v){return JSON.stringify(v)};function formatArgs(){var args=arguments;var useColors=this.useColors;args[0]=(useColors?"%c":"")+this.namespace+(useColors?" %c":" ")+args[0]+(useColors?"%c ":" ")+"+"+exports.humanize(this.diff);if(!useColors)return args;var c="color: "+this.color;args=[args[0],c,"color: inherit"].concat(Array.prototype.slice.call(args,1));var index=0;var lastC=0;args[0].replace(/%[a-z%]/g,function(match){if("%"===match)return;index++;if("%c"===match){lastC=index}});args.splice(lastC,0,c);return args}function log(){return"object"==typeof console&&"function"==typeof console.log&&Function.prototype.apply.call(console.log,console,arguments)}function save(namespaces){try{if(null==namespaces){localStorage.removeItem("debug")}else{localStorage.debug=namespaces}}catch(e){}}function load(){var r;try{r=localStorage.debug}catch(e){}return r}exports.enable(load())},{"./debug":22}],22:[function(_dereq_,module,exports){exports=module.exports=debug;exports.coerce=coerce;exports.disable=disable;exports.enable=enable;exports.enabled=enabled;exports.humanize=_dereq_("ms");exports.names=[];exports.skips=[];exports.formatters={};var prevColor=0;var prevTime;function selectColor(){return exports.colors[prevColor++%exports.colors.length]}function debug(namespace){function disabled(){}disabled.enabled=false;function enabled(){var self=enabled;var curr=+new Date;var ms=curr-(prevTime||curr);self.diff=ms;self.prev=prevTime;self.curr=curr;prevTime=curr;if(null==self.useColors)self.useColors=exports.useColors();if(null==self.color&&self.useColors)self.color=selectColor();var args=Array.prototype.slice.call(arguments);args[0]=exports.coerce(args[0]);if("string"!==typeof args[0]){args=["%o"].concat(args)}var index=0;args[0]=args[0].replace(/%([a-z%])/g,function(match,format){if(match==="%")return match;index++;var formatter=exports.formatters[format];if("function"===typeof formatter){var val=args[index];match=formatter.call(self,val);args.splice(index,1);index--}return match});if("function"===typeof exports.formatArgs){args=exports.formatArgs.apply(self,args)}var logFn=enabled.log||exports.log||console.log.bind(console);logFn.apply(self,args)}enabled.enabled=true;var fn=exports.enabled(namespace)?enabled:disabled;fn.namespace=namespace;return fn}function enable(namespaces){exports.save(namespaces);var split=(namespaces||"").split(/[\s,]+/);var len=split.length;for(var i=0;i=d)return Math.round(ms/d)+"d";if(ms>=h)return Math.round(ms/h)+"h";if(ms>=m)return Math.round(ms/m)+"m";if(ms>=s)return Math.round(ms/s)+"s";return ms+"ms"}function long(ms){return plural(ms,d,"day")||plural(ms,h,"hour")||plural(ms,m,"minute")||plural(ms,s,"second")||ms+" ms"}function plural(ms,n,name){if(ms1){return{type:packetslist[type],data:data.substring(1)}}else{return{type:packetslist[type]}}}var asArray=new Uint8Array(data);var type=asArray[0];var rest=sliceBuffer(data,1);if(Blob&&binaryType==="blob"){rest=new Blob([rest])}return{type:packetslist[type],data:rest}};exports.decodeBase64Packet=function(msg,binaryType){var type=packetslist[msg.charAt(0)];if(!global.ArrayBuffer){return{type:type,data:{base64:true,data:msg.substr(1)}}}var data=base64encoder.decode(msg.substr(1));if(binaryType==="blob"&&Blob){data=new Blob([data])}return{type:type,data:data}};exports.encodePayload=function(packets,supportsBinary,callback){if(typeof supportsBinary=="function"){callback=supportsBinary;supportsBinary=null}if(supportsBinary){if(Blob&&!isAndroid){return exports.encodePayloadAsBlob(packets,callback)}return exports.encodePayloadAsArrayBuffer(packets,callback)}if(!packets.length){return callback("0:")}function setLengthHeader(message){return message.length+":"+message}function encodeOne(packet,doneCallback){exports.encodePacket(packet,supportsBinary,true,function(message){doneCallback(null,setLengthHeader(message))})}map(packets,encodeOne,function(err,results){return callback(results.join(""))})};function map(ary,each,done){var result=new Array(ary.length);var next=after(ary.length,done);var eachWithIndex=function(i,el,cb){each(el,function(error,msg){result[i]=msg;cb(error,result)})};for(var i=0;i0){var tailArray=new Uint8Array(bufferTail);var isString=tailArray[0]===0;var msgLength="";for(var i=1;;i++){if(tailArray[i]==255)break;if(msgLength.length>310){numberTooLong=true;break}msgLength+=tailArray[i]}if(numberTooLong)return callback(err,0,1);bufferTail=sliceBuffer(bufferTail,2+msgLength.length);msgLength=parseInt(msgLength);var msg=sliceBuffer(bufferTail,0,msgLength);if(isString){try{msg=String.fromCharCode.apply(null,new Uint8Array(msg))}catch(e){var typed=new Uint8Array(msg);msg="";for(var i=0;ibytes){end=bytes}if(start>=bytes||start>=end||bytes===0){return new ArrayBuffer(0)}var abv=new Uint8Array(arraybuffer);var result=new Uint8Array(end-start);for(var i=start,ii=0;i>2];base64+=chars[(bytes[i]&3)<<4|bytes[i+1]>>4];base64+=chars[(bytes[i+1]&15)<<2|bytes[i+2]>>6];base64+=chars[bytes[i+2]&63]}if(len%3===2){base64=base64.substring(0,base64.length-1)+"="}else if(len%3===1){base64=base64.substring(0,base64.length-2)+"=="}return base64};exports.decode=function(base64){var bufferLength=base64.length*.75,len=base64.length,i,p=0,encoded1,encoded2,encoded3,encoded4;if(base64[base64.length-1]==="="){bufferLength--;if(base64[base64.length-2]==="="){bufferLength--}}var arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer);for(i=0;i>4;bytes[p++]=(encoded2&15)<<4|encoded3>>2;bytes[p++]=(encoded3&3)<<6|encoded4&63}return arraybuffer}})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")},{}],29:[function(_dereq_,module,exports){(function(global){var BlobBuilder=global.BlobBuilder||global.WebKitBlobBuilder||global.MSBlobBuilder||global.MozBlobBuilder;var blobSupported=function(){try{var b=new Blob(["hi"]);return b.size==2}catch(e){return false}}();var blobBuilderSupported=BlobBuilder&&BlobBuilder.prototype.append&&BlobBuilder.prototype.getBlob;function BlobBuilderConstructor(ary,options){options=options||{};var bb=new BlobBuilder;for(var i=0;i=55296&&value<=56319&&counter65535){value-=65536;output+=stringFromCharCode(value>>>10&1023|55296);value=56320|value&1023}output+=stringFromCharCode(value)}return output}function createByte(codePoint,shift){return stringFromCharCode(codePoint>>shift&63|128)}function encodeCodePoint(codePoint){if((codePoint&4294967168)==0){return stringFromCharCode(codePoint)}var symbol="";if((codePoint&4294965248)==0){symbol=stringFromCharCode(codePoint>>6&31|192)}else if((codePoint&4294901760)==0){symbol=stringFromCharCode(codePoint>>12&15|224);symbol+=createByte(codePoint,6)}else if((codePoint&4292870144)==0){symbol=stringFromCharCode(codePoint>>18&7|240);symbol+=createByte(codePoint,12);symbol+=createByte(codePoint,6)}symbol+=stringFromCharCode(codePoint&63|128);return symbol}function utf8encode(string){var codePoints=ucs2decode(string);var length=codePoints.length;var index=-1;var codePoint;var byteString="";while(++index=byteCount){throw Error("Invalid byte index")}var continuationByte=byteArray[byteIndex]&255;byteIndex++;if((continuationByte&192)==128){return continuationByte&63}throw Error("Invalid continuation byte")}function decodeSymbol(){var byte1;var byte2;var byte3;var byte4;var codePoint;if(byteIndex>byteCount){throw Error("Invalid byte index")}if(byteIndex==byteCount){return false}byte1=byteArray[byteIndex]&255;byteIndex++;if((byte1&128)==0){return byte1}if((byte1&224)==192){var byte2=readContinuationByte();codePoint=(byte1&31)<<6|byte2;if(codePoint>=128){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&240)==224){byte2=readContinuationByte();byte3=readContinuationByte();codePoint=(byte1&15)<<12|byte2<<6|byte3;if(codePoint>=2048){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&248)==240){byte2=readContinuationByte();byte3=readContinuationByte();byte4=readContinuationByte();codePoint=(byte1&15)<<18|byte2<<12|byte3<<6|byte4;if(codePoint>=65536&&codePoint<=1114111){return codePoint}}throw Error("Invalid UTF-8 detected")}var byteArray;var byteCount;var byteIndex;function utf8decode(byteString){byteArray=ucs2decode(byteString);byteCount=byteArray.length;byteIndex=0;var codePoints=[];var tmp;while((tmp=decodeSymbol())!==false){codePoints.push(tmp)}return ucs2encode(codePoints)}var utf8={version:"2.0.0",encode:utf8encode,decode:utf8decode};if(typeof define=="function"&&typeof define.amd=="object"&&define.amd){define(function(){return utf8})}else if(freeExports&&!freeExports.nodeType){if(freeModule){freeModule.exports=utf8}else{var object={};var hasOwnProperty=object.hasOwnProperty;for(var key in utf8){hasOwnProperty.call(utf8,key)&&(freeExports[key]=utf8[key])}}}else{root.utf8=utf8}})(this)}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],31:[function(_dereq_,module,exports){(function(global){var rvalidchars=/^[\],:{}\s]*$/;var rvalidescape=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rvalidtokens=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rvalidbraces=/(?:^|:|,)(?:\s*\[)+/g;var rtrimLeft=/^\s+/;var rtrimRight=/\s+$/;module.exports=function parsejson(data){if("string"!=typeof data||!data){return null}data=data.replace(rtrimLeft,"").replace(rtrimRight,"");if(global.JSON&&JSON.parse){return JSON.parse(data)}if(rvalidchars.test(data.replace(rvalidescape,"@").replace(rvalidtokens,"]").replace(rvalidbraces,""))){return new Function("return "+data)()}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],32:[function(_dereq_,module,exports){exports.encode=function(obj){var str="";for(var i in obj){if(obj.hasOwnProperty(i)){if(str.length)str+="&";str+=encodeURIComponent(i)+"="+encodeURIComponent(obj[i])}}return str};exports.decode=function(qs){var qry={};var pairs=qs.split("&");for(var i=0,l=pairs.length;i1)))/4)-floor((year-1901+month)/100)+floor((year-1601+month)/400)}}if(!(isProperty={}.hasOwnProperty)){isProperty=function(property){var members={},constructor;if((members.__proto__=null,members.__proto__={toString:1},members).toString!=getClass){isProperty=function(property){var original=this.__proto__,result=property in(this.__proto__=null,this);this.__proto__=original;return result}}else{constructor=members.constructor;isProperty=function(property){var parent=(this.constructor||constructor).prototype;return property in this&&!(property in parent&&this[property]===parent[property])}}members=null;return isProperty.call(this,property)}}var PrimitiveTypes={"boolean":1,number:1,string:1,undefined:1};var isHostType=function(object,property){var type=typeof object[property];return type=="object"?!!object[property]:!PrimitiveTypes[type]};forEach=function(object,callback){var size=0,Properties,members,property;(Properties=function(){this.valueOf=0}).prototype.valueOf=0;members=new Properties;for(property in members){if(isProperty.call(members,property)){size++}}Properties=members=null;if(!size){members=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"];forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,length;var hasProperty=!isFunction&&typeof object.constructor!="function"&&isHostType(object,"hasOwnProperty")?object.hasOwnProperty:isProperty;for(property in object){if(!(isFunction&&property=="prototype")&&hasProperty.call(object,property)){callback(property)}}for(length=members.length;property=members[--length];hasProperty.call(object,property)&&callback(property));}}else if(size==2){forEach=function(object,callback){var members={},isFunction=getClass.call(object)==functionClass,property;for(property in object){if(!(isFunction&&property=="prototype")&&!isProperty.call(members,property)&&(members[property]=1)&&isProperty.call(object,property)){callback(property)}}}}else{forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,isConstructor;for(property in object){if(!(isFunction&&property=="prototype")&&isProperty.call(object,property)&&!(isConstructor=property==="constructor")){callback(property)}}if(isConstructor||isProperty.call(object,property="constructor")){callback(property)}}}return forEach(object,callback)};if(!has("json-stringify")){var Escapes={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"};var leadingZeroes="000000";var toPaddedString=function(width,value){return(leadingZeroes+(value||0)).slice(-width)};var unicodePrefix="\\u00";var quote=function(value){var result='"',index=0,length=value.length,isLarge=length>10&&charIndexBuggy,symbols;if(isLarge){symbols=value.split("")}for(;index-1/0&&value<1/0){if(getDay){date=floor(value/864e5);for(year=floor(date/365.2425)+1970-1;getDay(year+1,0)<=date;year++);for(month=floor((date-getDay(year,0))/30.42);getDay(year,month+1)<=date;month++);date=1+date-getDay(year,month);time=(value%864e5+864e5)%864e5;hours=floor(time/36e5)%24;minutes=floor(time/6e4)%60;seconds=floor(time/1e3)%60;milliseconds=time%1e3}else{year=value.getUTCFullYear();month=value.getUTCMonth();date=value.getUTCDate();hours=value.getUTCHours();minutes=value.getUTCMinutes();seconds=value.getUTCSeconds();milliseconds=value.getUTCMilliseconds()}value=(year<=0||year>=1e4?(year<0?"-":"+")+toPaddedString(6,year<0?-year:year):toPaddedString(4,year))+"-"+toPaddedString(2,month+1)+"-"+toPaddedString(2,date)+"T"+toPaddedString(2,hours)+":"+toPaddedString(2,minutes)+":"+toPaddedString(2,seconds)+"."+toPaddedString(3,milliseconds)+"Z"}else{value=null}}else if(typeof value.toJSON=="function"&&(className!=numberClass&&className!=stringClass&&className!=arrayClass||isProperty.call(value,"toJSON"))){value=value.toJSON(property)}}if(callback){value=callback.call(object,property,value)}if(value===null){return"null"}className=getClass.call(value);if(className==booleanClass){return""+value}else if(className==numberClass){return value>-1/0&&value<1/0?""+value:"null"}else if(className==stringClass){return quote(""+value)}if(typeof value=="object"){for(length=stack.length;length--;){if(stack[length]===value){throw TypeError()}}stack.push(value);results=[];prefix=indentation;indentation+=whitespace;if(className==arrayClass){for(index=0,length=value.length;index0){for(whitespace="",width>10&&(width=10);whitespace.length=48&&charCode<=57||charCode>=97&&charCode<=102||charCode>=65&&charCode<=70)){abort()}}value+=fromCharCode("0x"+source.slice(begin,Index));break;default:abort()}}else{if(charCode==34){break}charCode=source.charCodeAt(Index);begin=Index;while(charCode>=32&&charCode!=92&&charCode!=34){charCode=source.charCodeAt(++Index)}value+=source.slice(begin,Index)}}if(source.charCodeAt(Index)==34){Index++;return value}abort();default:begin=Index;if(charCode==45){isSigned=true;charCode=source.charCodeAt(++Index)}if(charCode>=48&&charCode<=57){if(charCode==48&&(charCode=source.charCodeAt(Index+1),charCode>=48&&charCode<=57)){abort()}isSigned=false;for(;Index=48&&charCode<=57);Index++);if(source.charCodeAt(Index)==46){position=++Index;for(;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}charCode=source.charCodeAt(Index);if(charCode==101||charCode==69){charCode=source.charCodeAt(++Index);if(charCode==43||charCode==45){Index++}for(position=Index;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}return+source.slice(begin,Index)}if(isSigned){abort()}if(source.slice(Index,Index+4)=="true"){Index+=4;return true}else if(source.slice(Index,Index+5)=="false"){Index+=5;return false}else if(source.slice(Index,Index+4)=="null"){Index+=4;return null}abort()}}return"$"};var get=function(value){var results,hasMembers;if(value=="$"){abort()}if(typeof value=="string"){if((charIndexBuggy?value.charAt(0):value[0])=="@"){return value.slice(1)}if(value=="["){results=[];for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="]"){break}if(hasMembers){if(value==","){value=lex();if(value=="]"){abort()}}else{abort()}}if(value==","){abort()}results.push(get(value))}return results}else if(value=="{"){results={};for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="}"){break}if(hasMembers){if(value==","){value=lex();if(value=="}"){abort()}}else{abort()}}if(value==","||typeof value!="string"||(charIndexBuggy?value.charAt(0):value[0])!="@"||lex()!=":"){abort()}results[value.slice(1)]=get(lex())}return results}abort()}return value};var update=function(source,property,callback){var element=walk(source,property,callback);if(element===undef){delete source[property]}else{source[property]=element}};var walk=function(source,property,callback){var value=source[property],length;if(typeof value=="object"&&value){if(getClass.call(value)==arrayClass){for(length=value.length;length--;){update(value,length,callback)}}else{forEach(value,function(property){update(value,property,callback)})}}return callback.call(source,property,value)};JSON3.parse=function(source,callback){var result,value;Index=0;Source=""+source;result=get(lex());if(lex()!="$"){abort()}Index=Source=null;return callback&&getClass.call(callback)==functionClass?walk((value={},value[""]=result,value),"",callback):result}}}if(isLoader){define(function(){return JSON3})}})(this)},{}],47:[function(_dereq_,module,exports){module.exports=toArray;function toArray(list,index){var array=[];index=index||0;for(var i=index||0;i