From 0d4fa14fece357ba980730788563c3b38baca7d3 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sun, 10 Apr 2016 08:37:05 +0000 Subject: [PATCH] Implemented basic sup text support --- .babelrc | 7 + .eslintrc | 33 + app/client.js | 41 + app/controller/content.js | 98 + app/controller/menu.js | 23 + app/controller/store.js | 39 + app/main.js | 27 + app/socket.js | 5 + config.js | 60 + config/config.default.json | 23 + config/config.json | 1 + db.sqlite | Bin 0 -> 5120 bytes index.js | 38 + knexfile.js | 15 + log.js | 20 + migrations/20160410033515_base.js | 23 + nodemon.json | 3 + package.json | 47 +- public/client.css | 9 + public/client.html | 10 + public/client.js | 7309 ++++++++++++++++++++++ public/foundation.css | 2555 ++++++++ public/index.html | 15 + public/main.css | 4 + public/main.js | 9604 +++++++++++++++++++++++++++++ script/setup.js | 45 + server/bookshelf.js | 71 + server/index.js | 1 + server/io/connection.js | 34 + server/io/content/routes.js | 18 + server/io/router.js | 22 + server/io/store/model.js | 24 + server/io/store/routes.js | 11 + server/koa.js | 20 + server/middlewares.js | 40 + 35 files changed, 20292 insertions(+), 3 deletions(-) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 app/client.js create mode 100644 app/controller/content.js create mode 100644 app/controller/menu.js create mode 100644 app/controller/store.js create mode 100644 app/main.js create mode 100644 app/socket.js create mode 100644 config.js create mode 100644 config/config.default.json create mode 100644 config/config.json create mode 100644 db.sqlite create mode 100644 index.js create mode 100644 knexfile.js create mode 100644 log.js create mode 100644 migrations/20160410033515_base.js create mode 100644 nodemon.json create mode 100644 public/client.css create mode 100644 public/client.html create mode 100644 public/client.js create mode 100644 public/foundation.css create mode 100644 public/index.html create mode 100644 public/main.css create mode 100644 public/main.js create mode 100644 script/setup.js create mode 100644 server/bookshelf.js create mode 100644 server/index.js create mode 100644 server/io/connection.js create mode 100644 server/io/content/routes.js create mode 100644 server/io/router.js create mode 100644 server/io/store/model.js create mode 100644 server/io/store/routes.js create mode 100644 server/koa.js create mode 100644 server/middlewares.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..0d5884f --- /dev/null +++ b/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": ["es2015-node5"], + "plugins": [ + "transform-async-to-generator", + "syntax-async-functions" + ] +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..3ef31be --- /dev/null +++ b/.eslintrc @@ -0,0 +1,33 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb/base", + "ecmaFeatures": { + "modules": false + }, + "plugins": [ + "mocha" + ], + "rules": { + "mocha/no-exclusive-tests": 2, + "semi": [2, "never"], + "max-len": [1, 120], + "prefer-const": 0, + "consistent-return": 0, + "no-param-reassign": [2, {"props": false}], + "no-use-before-define": [2, {"functions": false, "classes": true}], + "no-unused-vars": [ + 2, + { + "args": "none" + } + ] + }, + "globals": { + "describe": false, + "it": false, + "before": false, + "beforeEach": false, + "after": false, + "afterEach": false + } +} diff --git a/app/client.js b/app/client.js new file mode 100644 index 0000000..f64accf --- /dev/null +++ b/app/client.js @@ -0,0 +1,41 @@ +const socket = require('./socket') + +socket.on('client.display', (data) => { + let exists = document.getElementById(data.key) + + if (exists) { + exists.tag.remove() + exists.remove() + } + + let element = document.createElement('div') + element.innerHTML = data.html + element.id = data.key + element.classList.add('root-element') + + let styleElement = document.createElement('style') + styleElement.setAttribute('type', 'text/css') + styleElement.innerHTML = data.css + + element.tag = styleElement + + document.body.appendChild(element) + document.head.appendChild(styleElement) + + window.setTimeout(() => { + element.classList.add('root-element-display') + }, 50) +}) + +socket.on('client.hide', (data) => { + let exists = document.getElementById(data.key) + + if (exists) { + exists.classList.remove('root-element-display') + + window.setTimeout(() => { + exists.tag.remove() + exists.remove() + }, 1500) + } +}) diff --git a/app/controller/content.js b/app/controller/content.js new file mode 100644 index 0000000..27c031e --- /dev/null +++ b/app/controller/content.js @@ -0,0 +1,98 @@ +const m = require('mithril') +const socket = require('../socket') +const store = require('./store') + +const Content = { } + +Content.vm = (function() { + let vm = {} + + vm.storeUpdated = function() { + vm.content = store.get('content') || {} + m.redraw() + } + + vm.init = function() { + vm.content = store.get('content') || {} + store.listen('content', vm.storeUpdated) + } + + vm.onunload = function() { + store.unlisten('content') + } + + vm.updated = function(name, control) { + vm.content[name] = control.target.value + store.set('content', vm.content) + } + + vm.display = function() { + socket.emit('content.display', vm.content) + } + + vm.hide = function() { + socket.emit('content.hide') + } + + return vm +})() + +Content.controller = function() { + Content.vm.init() + + this.onunload = Content.vm.onunload +} + +Content.view = function() { + return m('div', [ + m('h3', 'Content'), + m('div', { class: 'row' }, [ + m('div', { class: 'small-12 columns' }, [ + m('label', [ + 'HTML (use <%- name %> and <%- title %> for values)', + m('textarea', { + rows: '4', + oninput: Content.vm.updated.bind(null, 'html'), + value: Content.vm.content.html || '', + }) + ]), + ]), + m('div', { class: 'small-12 columns' }, [ + m('label', [ + 'CSS', + m('textarea', { + rows: '4', + oninput: Content.vm.updated.bind(null, 'css'), + value: Content.vm.content.css || '', + }) + ]), + ]), + m('div', { class: 'small-12 columns' }, [ + m('label', [ + 'Name', + m('input[type=text]', { + oninput: Content.vm.updated.bind(null, 'name'), + value: Content.vm.content.name || '', + }) + ]), + ]), + m('div', { class: 'small-12 columns' }, [ + m('label', [ + 'Title', + m('input[type=text]', { + oninput: Content.vm.updated.bind(null, 'title'), + value: Content.vm.content.title || '', + }) + ]), + ]), + m('a.button', { + onclick: Content.vm.display + }, 'Display'), + m('a.button.alert', { + onclick: Content.vm.hide + }, 'Hide'), + ]), + ]) +} + +module.exports = Content diff --git a/app/controller/menu.js b/app/controller/menu.js new file mode 100644 index 0000000..1bce181 --- /dev/null +++ b/app/controller/menu.js @@ -0,0 +1,23 @@ +const m = require('mithril') + +const Menu = { + controller: function() { + return {} + }, + + view: function(ctrl) { + return m('div', [ + m('h3', 'Menu'), + m('ul', [ + m('li', [ + m('a', { href: '/', config: m.route }, 'Home'), + ]), + m('li', [ + m('a', { href: '/content', config: m.route }, 'Content'), + ]) + ]), + ]) + }, +} + +module.exports = Menu diff --git a/app/controller/store.js b/app/controller/store.js new file mode 100644 index 0000000..2cb4ecd --- /dev/null +++ b/app/controller/store.js @@ -0,0 +1,39 @@ +const socket = require('../socket') +const storage = {} +const events = {} + +const store = { + get: function(name) { + return storage[name] + }, + + set: function(name, value, dontSend) { + storage[name] = value + + if (dontSend) { + if (events[name]) { + events[name]() + } + return + } + + socket.emit('store', { + name, + value, + }) + }, + + listen: function(name, caller) { + events[name] = caller + }, + + unlisten: function(name) { + delete events[name] + }, +} + +socket.on('store', (data) => { + store.set(data.name, data.value, true) +}) + +module.exports = store diff --git a/app/main.js b/app/main.js new file mode 100644 index 0000000..4363236 --- /dev/null +++ b/app/main.js @@ -0,0 +1,27 @@ +/** + * @license + * caspar-sup + * Copyright 2015 Jonatan Nilsson + * + * Available under WTFPL License (http://www.wtfpl.net/txt/copying/) +*/ + +'use strict' + +//Add debug components to window. Allows us to play with controls +//in the console. +window.components = {} + +require('./socket') +require('./controller/store') + +const m = require('mithril') +const Menu = require('./controller/menu') +const Content = require('./controller/content') + +m.mount(document.getElementById('menu'), Menu) + +m.route(document.getElementById('content'), '/', { + '/': {}, + '/content': Content, +}); diff --git a/app/socket.js b/app/socket.js new file mode 100644 index 0000000..62fd4e6 --- /dev/null +++ b/app/socket.js @@ -0,0 +1,5 @@ +const io = require('socket.io-client') + +const socket = io() + +module.exports = socket diff --git a/config.js b/config.js new file mode 100644 index 0000000..bee3f41 --- /dev/null +++ b/config.js @@ -0,0 +1,60 @@ +import _ from 'lodash' +import nconf from 'nconf' +const pckg = require('./package.json') + +// Helper method for global usage. +nconf.inTest = () => nconf.get('NODE_ENV') === 'test' + +// Config follow the following priority check order: +// 1. Arguments +// 2. package.json +// 3. Enviroment variables +// 4. config/config.json +// 5. default settings + + +// Load arguments as highest priority +nconf.argv() + + +// Load package.json for name and such +let project = _.pick(pckg, ['name', 'version', 'description', 'author', 'license', 'homepage']) + + +// If we have global.it, there's a huge chance +// we're in test mode so we force node_env to be test. +if (typeof global.it === 'function') { + project.NODE_ENV = 'test' +} + + +// Load overrides as second priority +nconf.overrides(project) + + +// Load enviroment variables as third priority +nconf.env() + + +// Load any overrides from the appropriate config file +let configFile = 'config/config.json' + +if (nconf.get('NODE_ENV') === 'test') { + configFile = 'config/config.test.json' +} + + +nconf.file('main', configFile) + +// Load defaults +nconf.file('default', 'config/config.default.json') + + +// Final sanity checks +/* istanbul ignore if */ +if (typeof global.it === 'function' & !nconf.inTest()) { + // eslint-disable-next-line no-console + console.log('Critical: potentially running test on production enviroment. Shutting down.') + process.exit(1) +} +module.exports = nconf diff --git a/config/config.default.json b/config/config.default.json new file mode 100644 index 0000000..c1c1327 --- /dev/null +++ b/config/config.default.json @@ -0,0 +1,23 @@ +{ + "NODE_ENV": "development", + "server": { + "port": 3000, + "host": "0.0.0.0" + }, + "bunyan": { + "name": "keywe", + "streams": [{ + "stream": "process.stdout", + "level": "debug" + } + ] + }, + "knex": { + "client": "sqlite3", + "connection": { + "filename" : "./db.sqlite" + }, + "migrations": { + } + } +} \ No newline at end of file diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/config/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/db.sqlite b/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..0d5de546410d8caad166e2ed3a8abe0de2399f3b GIT binary patch literal 5120 zcmeHL&2QQ;6n7G!<*>^x9Cms0gaiUwK9)&i42@0Ps(qEiDj^{gT<}O7nixb^Ra-0V zPuZWf%WhQdHe77n4oazdJYj#K{2Fh=mh{m z=u+Nsd0(3$o!E6lergN;-`q=7xVX3{M`L;d84dlX{zW>G-7y8Gv8_}prJcPIq-#@O zxL){(zk1(nw3^UryxeO74hAq}FzaDpT+lxCfV%?t*tTKhF^71^Fr>#o#=>RX@~A_( z$OGoYp%?_6s>X!a<5?@~7>Sf=YM+CJhn!A34(oeFFqius+qH)4`eMtp)7O2sUhoi6d|&PQL{|D8LitNpJVKalZDJ|c@0h!of`1@7xcF1d2j z*w&0xGC0kJzyDXrxZ1F^qjVw#A_e}bfUfD=!T#63A$dg>DR4IxIR79IvJbwdEmzhY zxH!QB;n=u}Ydtmr%O<}6yd9Jn3JO6Or(PS^+Z^B)LiX3v1yFzAAziQS$_TfCf(e_> zkb%Dd4X5BdwtQbwcr@b#Cu+G7>Gc@z7lb>iLPb_Tp0)XDoA0RQ=G;Q8td;Z`uRdR6dfZ(&x5J(Ot`eD;Y}p*)WoQVSMd1CSJhs!J8WR>`)gKNYesm?Vj}RF3%c E5Bq6HUH||9 literal 0 HcmV?d00001 diff --git a/index.js b/index.js new file mode 100644 index 0000000..0a08fff --- /dev/null +++ b/index.js @@ -0,0 +1,38 @@ +'use strict' +require('babel-register') + +let log = require('./log').default + +function exitHandler(options, err) { + if (options.cleanup) { + log.warn('Application is shutting down') + } + if (err) { + log.error('An unhandled error occured') + log.error(err) + } + if (options.exit) { + log.warn('Application is exiting') + process.exit() + } +} + +// do something when app is closing +process.on('exit', exitHandler.bind(null, { cleanup: true })) + +// catches ctrl+c event +process.on('SIGINT', exitHandler.bind(null, { exit: true })) + +// catches uncaught exceptions +process.on('uncaughtException', exitHandler.bind(null, { exit: true })) + +// Run the database script automatically. +log.info('Running database integrity scan.') +let setup = require('./script/setup') + +setup().then(() => { + require('./server') +}).catch((error) => { + log.error(error, 'Error while preparing database') + process.exit(1) +}) diff --git a/knexfile.js b/knexfile.js new file mode 100644 index 0000000..3699cb7 --- /dev/null +++ b/knexfile.js @@ -0,0 +1,15 @@ +'use strict' +require('babel-register') + +const _ = require('lodash') +const config = require('./config') + +let out = {} + +// This is important for setup to run cleanly. +let knexConfig = _.cloneDeep(config.get('knex')) +knexConfig.pool = { min: 1, max: 1 } + +out[config.get('NODE_ENV')] = knexConfig + +module.exports = out diff --git a/log.js b/log.js new file mode 100644 index 0000000..38d6d29 --- /dev/null +++ b/log.js @@ -0,0 +1,20 @@ +import _ from 'lodash' +import bunyan from 'bunyan' +import config from './config' + +// Clone the settings as we will be touching +// on them slightly. +let settings = _.cloneDeep(config.get('bunyan')) + +// Replace any instance of 'process.stdout' with the +// actual reference to the process.stdout. +for (let i = 0; i < settings.streams.length; i++) { + if (settings.streams[i].stream === 'process.stdout') { + settings.streams[i].stream = process.stdout + } +} + +// Create our logger. +const logger = bunyan.createLogger(settings) + +export default logger diff --git a/migrations/20160410033515_base.js b/migrations/20160410033515_base.js new file mode 100644 index 0000000..f9744e1 --- /dev/null +++ b/migrations/20160410033515_base.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +'use strict'; + +exports.up = function(knex, Promise) { + return Promise.all([ + knex.schema.createTable('store', function(table) { + table.increments() + table.text('name') + table.text('value') + }).then(() => { + return knex('store').insert({ + name: 'content', + value: '{}' + }) + }), + ]); +}; + +exports.down = function(knex, Promise) { + return Promise.all([ + knex.schema.dropTable('store'), + ]); +}; diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..4b74585 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["app/*", "public/*"] +} \ No newline at end of file diff --git a/package.json b/package.json index 98ba306..5d3a377 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,18 @@ { "name": "caspar-sup", "version": "1.0.0", - "description": "", + "description": "CasparCG superimposed graphics project", "main": "index.js", "scripts": { - "test": "npm test" + "test": "npm test", + "build-main:js": "browserify app/main.js -o public/main.js --debug", + "watch-main:js": "watchify app/main.js -o public/main.js --debug", + "build-client:js": "browserify app/client.js -o public/client.js --debug", + "watch-client:js": "watchify app/client.js -o public/client.js --debug", + "build": "npm run build-main:js && npm run build-client:js", + "build:watch": "parallelshell \"npm run watch-main:js\" \"npm run watch-client:js\"", + "start": "node index.js", + "start:dev": "nodemon index.js | bunyan" }, "repository": { "type": "git", @@ -20,5 +28,38 @@ "bugs": { "url": "https://github.com/nfp-projects/caspar-sup/issues" }, - "homepage": "https://github.com/nfp-projects/caspar-sup#readme" + "homepage": "https://github.com/nfp-projects/caspar-sup#readme", + "dependencies": { + "app-root-path": "^1.0.0", + "babel-plugin-syntax-async-functions": "^6.5.0", + "babel-plugin-transform-async-to-generator": "^6.7.0", + "babel-preset-es2015-node5": "^1.1.2", + "babel-register": "^6.7.2", + "bookshelf": "^0.9.2", + "browserify": "^13.0.0", + "bunyan": "^1.7.1", + "knex": "^0.10.0", + "koa": "^2.0.0-alpha.3", + "koa-socket": "^4.3.0", + "koa-static": "^3.0.0", + "lodash": "^4.6.1", + "mithril": "^0.2.3", + "nconf": "^0.8.4", + "parallelshell": "^2.0.0", + "socket.io": "^1.4.5", + "socket.io-client": "^1.4.5", + "sqlite3": "^3.1.3" + }, + "devDependencies": { + "assert-extended": "^1.0.1", + "babel-eslint": "^5.0.0", + "eslint": "^2.2.0", + "eslint-config-airbnb": "^6.1.0", + "eslint-plugin-mocha": "^2.0.0", + "live-reload": "^1.1.0", + "mocha": "^2.4.5", + "sinon": "^1.17.3", + "sinon-as-promised": "^4.0.0", + "watchify": "^3.7.0" + } } diff --git a/public/client.css b/public/client.css new file mode 100644 index 0000000..67db647 --- /dev/null +++ b/public/client.css @@ -0,0 +1,9 @@ +.root-element { + opacity: 0; + transition: opacity 1s; +} + +.root-element-display { + opacity: 1; + transition: opacity 1s; +} diff --git a/public/client.html b/public/client.html new file mode 100644 index 0000000..1bd9189 --- /dev/null +++ b/public/client.html @@ -0,0 +1,10 @@ + + + + CasparCG Client + + + + + + diff --git a/public/client.js b/public/client.js new file mode 100644 index 0000000..694057c --- /dev/null +++ b/public/client.js @@ -0,0 +1,7309 @@ +(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);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { + let exists = document.getElementById(data.key) + + if (exists) { + exists.tag.remove() + exists.remove() + } + + let element = document.createElement('div') + element.innerHTML = data.html + element.id = data.key + element.classList.add('root-element') + + let styleElement = document.createElement('style') + styleElement.setAttribute('type', 'text/css') + styleElement.innerHTML = data.css + + element.tag = styleElement + + document.body.appendChild(element) + document.head.appendChild(styleElement) + + window.setTimeout(() => { + element.classList.add('root-element-display') + }, 50) +}) + +socket.on('client.hide', (data) => { + let exists = document.getElementById(data.key) + + if (exists) { + exists.classList.remove('root-element-display') + + window.setTimeout(() => { + exists.tag.remove() + exists.remove() + }, 1500) + } +}) + +},{"./socket":2}],2:[function(require,module,exports){ +const io = require('socket.io-client') + +const socket = io() + +module.exports = socket + +},{"socket.io-client":36}],3:[function(require,module,exports){ +module.exports = after + +function after(count, callback, err_cb) { + var bail = false + err_cb = err_cb || noop + proxy.count = count + + return (count === 0) ? callback() : proxy + + function proxy(err, result) { + if (proxy.count <= 0) { + throw new Error('after called too many times') + } + --proxy.count + + // after first error, rest are passed to err_cb + if (err) { + bail = true + callback(err) + // future error callbacks will go to error handler + callback = err_cb + } else if (proxy.count === 0 && !bail) { + callback(null, result) + } + } +} + +function noop() {} + +},{}],4:[function(require,module,exports){ +/** + * An abstraction for slicing an arraybuffer even when + * ArrayBuffer.prototype.slice is not supported + * + * @api public + */ + +module.exports = function(arraybuffer, start, end) { + var bytes = arraybuffer.byteLength; + start = start || 0; + end = end || bytes; + + if (arraybuffer.slice) { return arraybuffer.slice(start, end); } + + if (start < 0) { start += bytes; } + if (end < 0) { end += bytes; } + if (end > bytes) { 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 < end; i++, ii++) { + result[ii] = abv[i]; + } + return result.buffer; +}; + +},{}],5:[function(require,module,exports){ + +/** + * Expose `Backoff`. + */ + +module.exports = Backoff; + +/** + * Initialize backoff timer with `opts`. + * + * - `min` initial timeout in milliseconds [100] + * - `max` max timeout [10000] + * - `jitter` [0] + * - `factor` [2] + * + * @param {Object} opts + * @api public + */ + +function Backoff(opts) { + opts = opts || {}; + this.ms = opts.min || 100; + this.max = opts.max || 10000; + this.factor = opts.factor || 2; + this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0; + this.attempts = 0; +} + +/** + * Return the backoff duration. + * + * @return {Number} + * @api public + */ + +Backoff.prototype.duration = function(){ + var ms = this.ms * Math.pow(this.factor, this.attempts++); + if (this.jitter) { + var rand = Math.random(); + var deviation = Math.floor(rand * this.jitter * ms); + ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation; + } + return Math.min(ms, this.max) | 0; +}; + +/** + * Reset the number of attempts. + * + * @api public + */ + +Backoff.prototype.reset = function(){ + this.attempts = 0; +}; + +/** + * Set the minimum duration + * + * @api public + */ + +Backoff.prototype.setMin = function(min){ + this.ms = min; +}; + +/** + * Set the maximum duration + * + * @api public + */ + +Backoff.prototype.setMax = function(max){ + this.max = max; +}; + +/** + * Set the jitter + * + * @api public + */ + +Backoff.prototype.setJitter = function(jitter){ + this.jitter = jitter; +}; + + +},{}],6:[function(require,module,exports){ +/* + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ +(function(chars){ + "use strict"; + + exports.encode = function(arraybuffer) { + var bytes = new Uint8Array(arraybuffer), + i, len = bytes.length, base64 = ""; + + for (i = 0; i < len; i+=3) { + base64 += chars[bytes[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 * 0.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 < len; i+=4) { + encoded1 = chars.indexOf(base64[i]); + encoded2 = chars.indexOf(base64[i+1]); + encoded3 = chars.indexOf(base64[i+2]); + encoded4 = chars.indexOf(base64[i+3]); + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; + }; +})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); + +},{}],7:[function(require,module,exports){ +(function (global){ +/** + * Create a blob builder even when vendor prefixes exist + */ + +var BlobBuilder = global.BlobBuilder + || global.WebKitBlobBuilder + || global.MSBlobBuilder + || global.MozBlobBuilder; + +/** + * Check if Blob constructor is supported + */ + +var blobSupported = (function() { + try { + var a = new Blob(['hi']); + return a.size === 2; + } catch(e) { + return false; + } +})(); + +/** + * Check if Blob constructor supports ArrayBufferViews + * Fails in Safari 6, so we need to map to ArrayBuffers there. + */ + +var blobSupportsArrayBufferView = blobSupported && (function() { + try { + var b = new Blob([new Uint8Array([1,2])]); + return b.size === 2; + } catch(e) { + return false; + } +})(); + +/** + * Check if BlobBuilder is supported + */ + +var blobBuilderSupported = BlobBuilder + && BlobBuilder.prototype.append + && BlobBuilder.prototype.getBlob; + +/** + * Helper function that maps ArrayBufferViews to ArrayBuffers + * Used by BlobBuilder constructor and old browsers that didn't + * support it in the Blob constructor. + */ + +function mapArrayBufferViews(ary) { + for (var i = 0; i < ary.length; i++) { + var chunk = ary[i]; + if (chunk.buffer instanceof ArrayBuffer) { + var buf = chunk.buffer; + + // if this is a subarray, make a copy so we only + // include the subarray region from the underlying buffer + if (chunk.byteLength !== buf.byteLength) { + var copy = new Uint8Array(chunk.byteLength); + copy.set(new Uint8Array(buf, chunk.byteOffset, chunk.byteLength)); + buf = copy.buffer; + } + + ary[i] = buf; + } + } +} + +function BlobBuilderConstructor(ary, options) { + options = options || {}; + + var bb = new BlobBuilder(); + mapArrayBufferViews(ary); + + for (var i = 0; i < ary.length; i++) { + bb.append(ary[i]); + } + + return (options.type) ? bb.getBlob(options.type) : bb.getBlob(); +}; + +function BlobConstructor(ary, options) { + mapArrayBufferViews(ary); + return new Blob(ary, options || {}); +}; + +module.exports = (function() { + if (blobSupported) { + return blobSupportsArrayBufferView ? global.Blob : BlobConstructor; + } else if (blobBuilderSupported) { + return BlobBuilderConstructor; + } else { + return undefined; + } +})(); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) + +},{}],8:[function(require,module,exports){ + +},{}],9:[function(require,module,exports){ +/** + * Slice reference. + */ + +var slice = [].slice; + +/** + * Bind `obj` to `fn`. + * + * @param {Object} obj + * @param {Function|String} fn or string + * @return {Function} + * @api public + */ + +module.exports = function(obj, fn){ + if ('string' == typeof fn) fn = obj[fn]; + if ('function' != typeof fn) throw new Error('bind() requires a function'); + var args = slice.call(arguments, 2); + return function(){ + return fn.apply(obj, args.concat(slice.call(arguments))); + } +}; + +},{}],10:[function(require,module,exports){ + +/** + * Expose `Emitter`. + */ + +module.exports = Emitter; + +/** + * Initialize a new `Emitter`. + * + * @api public + */ + +function Emitter(obj) { + if (obj) return mixin(obj); +}; + +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; +} + +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.on = +Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; +}; + +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; + + function on() { + self.off(event, on); + fn.apply(this, arguments); + } + + on.fn = fn; + this.on(event, on); + return this; +}; + +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.off = +Emitter.prototype.removeListener = +Emitter.prototype.removeAllListeners = +Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } + + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; + + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } + + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; +}; + +/** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ + +Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; + + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } + + return this; +}; + +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ + +Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; +}; + +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + +Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; +}; + +},{}],11:[function(require,module,exports){ + +module.exports = function(a, b){ + var fn = function(){}; + fn.prototype = b.prototype; + a.prototype = new fn; + a.prototype.constructor = a; +}; +},{}],12:[function(require,module,exports){ + +/** + * This is the web browser implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = require('./debug'); +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; +exports.storage = 'undefined' != typeof chrome + && 'undefined' != typeof chrome.storage + ? chrome.storage.local + : localstorage(); + +/** + * Colors. + */ + +exports.colors = [ + 'lightseagreen', + 'forestgreen', + 'goldenrod', + 'dodgerblue', + 'darkorchid', + 'crimson' +]; + +/** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // is webkit? http://stackoverflow.com/a/16459606/376773 + return ('WebkitAppearance' in document.documentElement.style) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (window.console && (console.firebug || (console.exception && console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + return JSON.stringify(v); +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +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)); + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); + return args; +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage(){ + try { + return window.localStorage; + } catch (e) {} +} + +},{"./debug":13}],13:[function(require,module,exports){ + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = debug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = require('ms'); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lowercased letter, i.e. "n". + */ + +exports.formatters = {}; + +/** + * Previously assigned color. + */ + +var prevColor = 0; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * + * @return {Number} + * @api private + */ + +function selectColor() { + return exports.colors[prevColor++ % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function debug(namespace) { + + // define the `disabled` version + function disabled() { + } + disabled.enabled = false; + + // define the `enabled` version + function enabled() { + + var self = enabled; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // add the `color` if not set + 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]) { + // anything else let's inspect with %o + args = ['%o'].concat(args); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + 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; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + var split = (namespaces || '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +},{"ms":32}],14:[function(require,module,exports){ + +module.exports = require('./lib/'); + +},{"./lib/":15}],15:[function(require,module,exports){ + +module.exports = require('./socket'); + +/** + * Exports parser + * + * @api public + * + */ +module.exports.parser = require('engine.io-parser'); + +},{"./socket":16,"engine.io-parser":24}],16:[function(require,module,exports){ +(function (global){ +/** + * Module dependencies. + */ + +var transports = require('./transports'); +var Emitter = require('component-emitter'); +var debug = require('debug')('engine.io-client:socket'); +var index = require('indexof'); +var parser = require('engine.io-parser'); +var parseuri = require('parseuri'); +var parsejson = require('parsejson'); +var parseqs = require('parseqs'); + +/** + * Module exports. + */ + +module.exports = Socket; + +/** + * Noop function. + * + * @api private + */ + +function noop(){} + +/** + * Socket constructor. + * + * @param {String|Object} uri or options + * @param {Object} options + * @api public + */ + +function Socket(uri, opts){ + if (!(this instanceof Socket)) return new Socket(uri, opts); + + opts = opts || {}; + + if (uri && 'object' == typeof uri) { + opts = uri; + uri = null; + } + + if (uri) { + uri = parseuri(uri); + opts.hostname = uri.host; + opts.secure = uri.protocol == 'https' || uri.protocol == 'wss'; + opts.port = uri.port; + if (uri.query) opts.query = uri.query; + } else if (opts.host) { + opts.hostname = parseuri(opts.host).host; + } + + this.secure = null != opts.secure ? opts.secure : + (global.location && 'https:' == location.protocol); + + if (opts.hostname && !opts.port) { + // if no port is specified manually, use the protocol default + opts.port = this.secure ? '443' : '80'; + } + + this.agent = opts.agent || false; + this.hostname = opts.hostname || + (global.location ? location.hostname : 'localhost'); + this.port = opts.port || (global.location && location.port ? + location.port : + (this.secure ? 443 : 80)); + this.query = opts.query || {}; + if ('string' == typeof this.query) this.query = parseqs.decode(this.query); + this.upgrade = false !== opts.upgrade; + this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/'; + this.forceJSONP = !!opts.forceJSONP; + this.jsonp = false !== opts.jsonp; + this.forceBase64 = !!opts.forceBase64; + this.enablesXDR = !!opts.enablesXDR; + this.timestampParam = opts.timestampParam || 't'; + this.timestampRequests = opts.timestampRequests; + this.transports = opts.transports || ['polling', 'websocket']; + this.readyState = ''; + this.writeBuffer = []; + this.policyPort = opts.policyPort || 843; + this.rememberUpgrade = opts.rememberUpgrade || false; + this.binaryType = null; + this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades; + this.perMessageDeflate = false !== opts.perMessageDeflate ? (opts.perMessageDeflate || {}) : false; + + if (true === this.perMessageDeflate) this.perMessageDeflate = {}; + if (this.perMessageDeflate && null == this.perMessageDeflate.threshold) { + this.perMessageDeflate.threshold = 1024; + } + + // SSL options for Node.js client + this.pfx = opts.pfx || null; + this.key = opts.key || null; + this.passphrase = opts.passphrase || null; + this.cert = opts.cert || null; + this.ca = opts.ca || null; + this.ciphers = opts.ciphers || null; + this.rejectUnauthorized = opts.rejectUnauthorized === undefined ? null : opts.rejectUnauthorized; + + // other options for Node.js client + var freeGlobal = typeof global == 'object' && global; + if (freeGlobal.global === freeGlobal) { + if (opts.extraHeaders && Object.keys(opts.extraHeaders).length > 0) { + this.extraHeaders = opts.extraHeaders; + } + } + + this.open(); +} + +Socket.priorWebsocketSuccess = false; + +/** + * Mix in `Emitter`. + */ + +Emitter(Socket.prototype); + +/** + * Protocol version. + * + * @api public + */ + +Socket.protocol = parser.protocol; // this is an int + +/** + * Expose deps for legacy compatibility + * and standalone browser access. + */ + +Socket.Socket = Socket; +Socket.Transport = require('./transport'); +Socket.transports = require('./transports'); +Socket.parser = require('engine.io-parser'); + +/** + * Creates transport of the given type. + * + * @param {String} transport name + * @return {Transport} + * @api private + */ + +Socket.prototype.createTransport = function (name) { + debug('creating transport "%s"', name); + var query = clone(this.query); + + // append engine.io protocol identifier + query.EIO = parser.protocol; + + // transport name + query.transport = name; + + // session id if we already have one + if (this.id) query.sid = this.id; + + var transport = new transports[name]({ + agent: this.agent, + hostname: this.hostname, + port: this.port, + secure: this.secure, + path: this.path, + query: query, + forceJSONP: this.forceJSONP, + jsonp: this.jsonp, + forceBase64: this.forceBase64, + enablesXDR: this.enablesXDR, + timestampRequests: this.timestampRequests, + timestampParam: this.timestampParam, + policyPort: this.policyPort, + socket: this, + pfx: this.pfx, + key: this.key, + passphrase: this.passphrase, + cert: this.cert, + ca: this.ca, + ciphers: this.ciphers, + rejectUnauthorized: this.rejectUnauthorized, + perMessageDeflate: this.perMessageDeflate, + extraHeaders: this.extraHeaders + }); + + return transport; +}; + +function clone (obj) { + var o = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + o[i] = obj[i]; + } + } + return o; +} + +/** + * Initializes transport to use and starts probe. + * + * @api private + */ +Socket.prototype.open = function () { + var transport; + if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) { + transport = 'websocket'; + } else if (0 === this.transports.length) { + // Emit error on next tick so it can be listened to + var self = this; + setTimeout(function() { + self.emit('error', 'No transports available'); + }, 0); + return; + } else { + transport = this.transports[0]; + } + this.readyState = 'opening'; + + // Retry with the next transport if the transport is disabled (jsonp: false) + try { + transport = this.createTransport(transport); + } catch (e) { + this.transports.shift(); + this.open(); + return; + } + + transport.open(); + this.setTransport(transport); +}; + +/** + * Sets the current transport. Disables the existing one (if any). + * + * @api private + */ + +Socket.prototype.setTransport = function(transport){ + debug('setting transport %s', transport.name); + var self = this; + + if (this.transport) { + debug('clearing existing transport %s', this.transport.name); + this.transport.removeAllListeners(); + } + + // set up transport + this.transport = transport; + + // set up transport listeners + transport + .on('drain', function(){ + self.onDrain(); + }) + .on('packet', function(packet){ + self.onPacket(packet); + }) + .on('error', function(e){ + self.onError(e); + }) + .on('close', function(){ + self.onClose('transport close'); + }); +}; + +/** + * Probes a transport. + * + * @param {String} transport name + * @api private + */ + +Socket.prototype.probe = function (name) { + debug('probing transport "%s"', name); + var transport = this.createTransport(name, { probe: 1 }) + , failed = false + , self = this; + + Socket.priorWebsocketSuccess = false; + + function onTransportOpen(){ + if (self.onlyBinaryUpgrades) { + var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary; + failed = failed || upgradeLosesBinary; + } + if (failed) return; + + debug('probe transport "%s" opened', name); + transport.send([{ type: 'ping', data: 'probe' }]); + transport.once('packet', function (msg) { + if (failed) return; + if ('pong' == msg.type && 'probe' == msg.data) { + debug('probe transport "%s" pong', name); + self.upgrading = true; + self.emit('upgrading', transport); + if (!transport) return; + Socket.priorWebsocketSuccess = 'websocket' == transport.name; + + debug('pausing current transport "%s"', self.transport.name); + self.transport.pause(function () { + if (failed) return; + if ('closed' == self.readyState) return; + debug('changing transport and sending upgrade packet'); + + cleanup(); + + self.setTransport(transport); + transport.send([{ type: 'upgrade' }]); + self.emit('upgrade', transport); + transport = null; + self.upgrading = false; + self.flush(); + }); + } else { + debug('probe transport "%s" failed', name); + var err = new Error('probe error'); + err.transport = transport.name; + self.emit('upgradeError', err); + } + }); + } + + function freezeTransport() { + if (failed) return; + + // Any callback called by transport should be ignored since now + failed = true; + + cleanup(); + + transport.close(); + transport = null; + } + + //Handle any error that happens while probing + function onerror(err) { + var error = new Error('probe error: ' + err); + error.transport = transport.name; + + freezeTransport(); + + debug('probe transport "%s" failed because of error: %s', name, err); + + self.emit('upgradeError', error); + } + + function onTransportClose(){ + onerror("transport closed"); + } + + //When the socket is closed while we're probing + function onclose(){ + onerror("socket closed"); + } + + //When the socket is upgraded while we're probing + function onupgrade(to){ + if (transport && to.name != transport.name) { + debug('"%s" works - aborting "%s"', to.name, transport.name); + freezeTransport(); + } + } + + //Remove all listeners on the transport and on self + function cleanup(){ + transport.removeListener('open', onTransportOpen); + transport.removeListener('error', onerror); + transport.removeListener('close', onTransportClose); + self.removeListener('close', onclose); + self.removeListener('upgrading', onupgrade); + } + + transport.once('open', onTransportOpen); + transport.once('error', onerror); + transport.once('close', onTransportClose); + + this.once('close', onclose); + this.once('upgrading', onupgrade); + + transport.open(); + +}; + +/** + * Called when connection is deemed open. + * + * @api public + */ + +Socket.prototype.onOpen = function () { + debug('socket open'); + this.readyState = 'open'; + Socket.priorWebsocketSuccess = 'websocket' == this.transport.name; + this.emit('open'); + this.flush(); + + // we check for `readyState` in case an `open` + // listener already closed the socket + if ('open' == this.readyState && this.upgrade && this.transport.pause) { + debug('starting upgrade probes'); + for (var i = 0, l = this.upgrades.length; i < l; i++) { + this.probe(this.upgrades[i]); + } + } +}; + +/** + * Handles a packet. + * + * @api private + */ + +Socket.prototype.onPacket = function (packet) { + if ('opening' == this.readyState || 'open' == this.readyState) { + debug('socket receive: type "%s", data "%s"', packet.type, packet.data); + + this.emit('packet', packet); + + // Socket is live - any packet counts + this.emit('heartbeat'); + + switch (packet.type) { + case 'open': + this.onHandshake(parsejson(packet.data)); + break; + + case 'pong': + this.setPing(); + this.emit('pong'); + break; + + case 'error': + var err = new Error('server error'); + err.code = packet.data; + this.onError(err); + break; + + case 'message': + this.emit('data', packet.data); + this.emit('message', packet.data); + break; + } + } else { + debug('packet received with socket readyState "%s"', this.readyState); + } +}; + +/** + * Called upon handshake completion. + * + * @param {Object} handshake obj + * @api private + */ + +Socket.prototype.onHandshake = function (data) { + this.emit('handshake', data); + this.id = data.sid; + this.transport.query.sid = data.sid; + this.upgrades = this.filterUpgrades(data.upgrades); + this.pingInterval = data.pingInterval; + this.pingTimeout = data.pingTimeout; + this.onOpen(); + // In case open handler closes socket + if ('closed' == this.readyState) return; + this.setPing(); + + // Prolong liveness of socket on heartbeat + this.removeListener('heartbeat', this.onHeartbeat); + this.on('heartbeat', this.onHeartbeat); +}; + +/** + * Resets ping timeout. + * + * @api private + */ + +Socket.prototype.onHeartbeat = function (timeout) { + clearTimeout(this.pingTimeoutTimer); + var self = this; + self.pingTimeoutTimer = setTimeout(function () { + if ('closed' == self.readyState) return; + self.onClose('ping timeout'); + }, timeout || (self.pingInterval + self.pingTimeout)); +}; + +/** + * Pings server every `this.pingInterval` and expects response + * within `this.pingTimeout` or closes connection. + * + * @api private + */ + +Socket.prototype.setPing = function () { + var self = this; + clearTimeout(self.pingIntervalTimer); + self.pingIntervalTimer = setTimeout(function () { + debug('writing ping packet - expecting pong within %sms', self.pingTimeout); + self.ping(); + self.onHeartbeat(self.pingTimeout); + }, self.pingInterval); +}; + +/** +* Sends a ping packet. +* +* @api private +*/ + +Socket.prototype.ping = function () { + var self = this; + this.sendPacket('ping', function(){ + self.emit('ping'); + }); +}; + +/** + * Called on `drain` event + * + * @api private + */ + +Socket.prototype.onDrain = function() { + this.writeBuffer.splice(0, this.prevBufferLen); + + // setting prevBufferLen = 0 is very important + // for example, when upgrading, upgrade packet is sent over, + // and a nonzero prevBufferLen could cause problems on `drain` + this.prevBufferLen = 0; + + if (0 === this.writeBuffer.length) { + this.emit('drain'); + } else { + this.flush(); + } +}; + +/** + * Flush write buffers. + * + * @api private + */ + +Socket.prototype.flush = function () { + if ('closed' != this.readyState && this.transport.writable && + !this.upgrading && this.writeBuffer.length) { + debug('flushing %d packets in socket', this.writeBuffer.length); + this.transport.send(this.writeBuffer); + // keep track of current length of writeBuffer + // splice writeBuffer and callbackBuffer on `drain` + this.prevBufferLen = this.writeBuffer.length; + this.emit('flush'); + } +}; + +/** + * Sends a message. + * + * @param {String} message. + * @param {Function} callback function. + * @param {Object} options. + * @return {Socket} for chaining. + * @api public + */ + +Socket.prototype.write = +Socket.prototype.send = function (msg, options, fn) { + this.sendPacket('message', msg, options, fn); + return this; +}; + +/** + * Sends a packet. + * + * @param {String} packet type. + * @param {String} data. + * @param {Object} options. + * @param {Function} callback function. + * @api private + */ + +Socket.prototype.sendPacket = function (type, data, options, fn) { + if('function' == typeof data) { + fn = data; + data = undefined; + } + + if ('function' == typeof options) { + fn = options; + options = null; + } + + if ('closing' == this.readyState || 'closed' == this.readyState) { + return; + } + + options = options || {}; + options.compress = false !== options.compress; + + var packet = { + type: type, + data: data, + options: options + }; + this.emit('packetCreate', packet); + this.writeBuffer.push(packet); + if (fn) this.once('flush', fn); + this.flush(); +}; + +/** + * Closes the connection. + * + * @api private + */ + +Socket.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.readyState = 'closing'; + + var self = this; + + if (this.writeBuffer.length) { + this.once('drain', function() { + if (this.upgrading) { + waitForUpgrade(); + } else { + close(); + } + }); + } else if (this.upgrading) { + waitForUpgrade(); + } else { + close(); + } + } + + function close() { + self.onClose('forced close'); + debug('socket closing - telling transport to close'); + self.transport.close(); + } + + function cleanupAndClose() { + self.removeListener('upgrade', cleanupAndClose); + self.removeListener('upgradeError', cleanupAndClose); + close(); + } + + function waitForUpgrade() { + // wait for upgrade to finish since we can't send packets while pausing a transport + self.once('upgrade', cleanupAndClose); + self.once('upgradeError', cleanupAndClose); + } + + return this; +}; + +/** + * Called upon transport error + * + * @api private + */ + +Socket.prototype.onError = function (err) { + debug('socket error %j', err); + Socket.priorWebsocketSuccess = false; + this.emit('error', err); + this.onClose('transport error', err); +}; + +/** + * Called upon transport close. + * + * @api private + */ + +Socket.prototype.onClose = function (reason, desc) { + if ('opening' == this.readyState || 'open' == this.readyState || 'closing' == this.readyState) { + debug('socket close with reason: "%s"', reason); + var self = this; + + // clear timers + clearTimeout(this.pingIntervalTimer); + clearTimeout(this.pingTimeoutTimer); + + // stop event from firing again for transport + this.transport.removeAllListeners('close'); + + // ensure transport won't stay open + this.transport.close(); + + // ignore further transport communication + this.transport.removeAllListeners(); + + // set ready state + this.readyState = 'closed'; + + // clear session id + this.id = null; + + // emit close event + this.emit('close', reason, desc); + + // clean buffers after, so users can still + // grab the buffers on `close` event + self.writeBuffer = []; + self.prevBufferLen = 0; + } +}; + +/** + * Filters upgrades, returning only those matching client transports. + * + * @param {Array} server upgrades + * @api private + * + */ + +Socket.prototype.filterUpgrades = function (upgrades) { + var filteredUpgrades = []; + for (var i = 0, j = upgrades.length; i