├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib └── sockjs-client.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *~ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2012-2013 VMware, Inc. 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **DEPRECATED** 2 | 3 | The [sockjs-client](https://github.com/sockjs/sockjs-client) project now supports running in node, and supports more transports than this project does. I strongly encourage you to use that instead. 4 | 5 | # SockJS Client Node 6 | 7 | Node client for [SockJS](https://github.com/sockjs). Currently, only 8 | the XHR Streaming transport is supported. 9 | 10 | ## Usage 11 | 12 | var sjsc = require('sockjs-client'); 13 | var client = sjsc.create("http://localhost/sjsServer"); 14 | client.on('connection', function () { // connection is established }); 15 | client.on('data', function (msg) { // received some data }); 16 | client.on('error', function (e) { // something went wrong }); 17 | client.write("Have some text you mighty SockJS server!"); 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/sockjs-client'); 2 | -------------------------------------------------------------------------------- /lib/sockjs-client.js: -------------------------------------------------------------------------------- 1 | (function (parent) { 2 | 'use strict'; 3 | 4 | var url = require('url'), 5 | http = require('http'), 6 | https = require('https'), 7 | uuid = require('node-uuid'), 8 | events = require('events'), 9 | util; 10 | 11 | function InvalidURL (parsedURL) { 12 | this.parsedURL = parsedURL; 13 | } 14 | InvalidURL.prototype = { 15 | prototype: Error.prototype, 16 | toString: function () { return "Invalid URL: " + this.parsedURL.href; } 17 | }; 18 | 19 | function InvalidState (extra) { 20 | this.extra = extra; 21 | } 22 | InvalidState.prototype = { 23 | prototype: Error.prototype, 24 | toString: function () { return "Invalid State " + this.extra; } 25 | }; 26 | 27 | util = (function () { 28 | var empty = {}; 29 | return { 30 | hasOwnProperty: function (obj, field) { 31 | return empty.hasOwnProperty.call(obj, field); 32 | }, 33 | 34 | shallowCopy: function (src, dest) { 35 | var keys = Object.keys(src), 36 | i; 37 | for (i = 0; i < keys.length; i += 1) { 38 | dest[keys[i]] = src[keys[i]]; 39 | } 40 | }, 41 | 42 | liftFunctions: function (src, dest, fields) { 43 | var i, field; 44 | for (i = 0; i < fields.length; i += 1) { 45 | field = fields[i]; 46 | if (undefined !== src[field] && 47 | undefined !== src[field].call) { 48 | dest[field] = src[field].bind(src); 49 | } 50 | } 51 | } 52 | }; 53 | }()); 54 | 55 | function SockJSClient (server) { 56 | var parsed, serverId, sessionId; 57 | 58 | parsed = url.parse(server); 59 | 60 | if ('http:' === parsed.protocol) { 61 | this.client = http; 62 | } else if ('https:' === parsed.protocol) { 63 | this.client = https; 64 | } else { 65 | throw new InvalidURL(parsed); 66 | } 67 | 68 | if (parsed.pathname === '/') { 69 | parsed.pathname = ''; 70 | } 71 | 72 | serverId = Math.round(Math.random() * 999); 73 | sessionId = uuid(); 74 | 75 | this.server = url.parse( 76 | parsed.protocol + "//" + parsed.host + parsed.pathname + 77 | "/" + serverId + "/" + sessionId); 78 | 79 | this.error = Object.getPrototypeOf(this).error.bind(this); 80 | this.connection = Object.getPrototypeOf(this).connection.bind(this); 81 | this.closed = Object.getPrototypeOf(this).closed.bind(this); 82 | 83 | this.emitter = new events.EventEmitter(); 84 | util.liftFunctions( 85 | this.emitter, this, 86 | ['on', 'once', 'removeListener', 'removeAllListeners', 'emit']); 87 | 88 | this.writeBuffer = []; 89 | } 90 | 91 | SockJSClient.prototype = { 92 | isReady: false, 93 | isClosing: false, 94 | isClosed: false, 95 | 96 | connect: function () { 97 | if (this.isReady || this.isClosing || this.isClosed) { 98 | return; 99 | } 100 | var transport = new XHRStreaming(this); 101 | transport.on('error', this.error); 102 | transport.on('connection', this.connection); 103 | transport.on('close', this.closed); 104 | (new StateMachine(transport)).invoke(); 105 | }, 106 | 107 | connection: function (transport) { 108 | if (this.isClosing) { 109 | transport.close(); 110 | } else if (! (this.isReady || this.isClosed)) { 111 | this.isReady = true; 112 | this.transport = transport; 113 | this.emit('connection'); 114 | if (0 !== this.writeBuffer.length) { 115 | transport.write(this.writeBuffer); 116 | this.writeBuffer = []; 117 | } 118 | } 119 | }, 120 | 121 | error: function () { 122 | this.isReady = false; 123 | var args = Array.prototype.slice.call(arguments, 0); 124 | args.unshift('error'); 125 | this.emit.apply(this, args); 126 | if (this.isClosing) { 127 | this.closed(); 128 | } 129 | }, 130 | 131 | write: function (message) { 132 | if (this.isClosed || this.isClosing) { 133 | return; 134 | } else if (this.isReady) { 135 | return this.transport.write([message]); 136 | } else { 137 | this.writeBuffer.push(message); 138 | } 139 | }, 140 | 141 | close: function () { 142 | if (! (this.isClosing || this.isClosed)) { 143 | this.isClosing = true; 144 | if (this.isReady) { 145 | this.isReady = false; 146 | this.transport.close(); 147 | } 148 | } 149 | }, 150 | 151 | closed: function () { 152 | if (! this.isClosed) { 153 | var args = Array.prototype.slice.call(arguments, 0); 154 | args.unshift('close'); 155 | this.emit.apply(this, args); 156 | } 157 | this.isClosed = true; 158 | this.isClosing = false; 159 | this.isReady = false; 160 | } 161 | }; 162 | 163 | function XHRStreaming (sjs) { 164 | this.sjs = sjs; 165 | this.emitter = new events.EventEmitter(); 166 | util.liftFunctions( 167 | this.emitter, this, 168 | ['on', 'once', 'removeListener', 'removeAllListeners', 'emit']); 169 | this.error = Object.getPrototypeOf(this).error.bind(this); 170 | this.initialPayloadRemaining = this.initialPayloadLength; 171 | this.partialChunk = ""; 172 | } 173 | XHRStreaming.prototype = { 174 | fsm: {'start': 'connected', 175 | 'connected': 'dataInitial', 176 | 'dataInitial': 'dataOpen', 177 | 'dataOpen': 'running', 178 | 'running': 'running', 179 | 'errored': 'errored' 180 | }, 181 | 182 | initialPayloadLength: 2049, 183 | 184 | start: function (sm) { 185 | var request = {method: 'POST', 186 | headers: {'Content-Length': 0}}, 187 | clientRequest; 188 | util.shallowCopy(this.sjs.server, request); 189 | request.path += '/xhr_streaming'; 190 | clientRequest = this.sjs.client.request(request, sm.stepper()); 191 | clientRequest.on('error', this.error.bind(this, sm)); 192 | clientRequest.end(); 193 | }, 194 | 195 | write: function (message) { 196 | var data = JSON.stringify(message), 197 | request = {method: 'POST', 198 | headers: { 199 | 'Content-Type': 'application/json', 200 | 'Content-Length': Buffer.byteLength(data,'utf8')}}, 201 | clientRequest; 202 | util.shallowCopy(this.sjs.server, request); 203 | request.path += '/xhr_send'; 204 | clientRequest = this.sjs.client.request(request); 205 | clientRequest.write(data); 206 | clientRequest.end(); 207 | }, 208 | 209 | close: function () { 210 | if (undefined !== this.response) { 211 | this.response.removeAllListeners(); 212 | this.response.destroy(); 213 | } 214 | this.emit('close'); 215 | }, 216 | 217 | connected: function (sm, result) { 218 | this.response = result; 219 | if (200 !== result.statusCode) { 220 | this.error(sm, result.statusCode); 221 | } else { 222 | result.setEncoding('utf8'); 223 | result.on('data', sm.stepper()); 224 | result.on('end', this.reopen.bind(this, sm)); 225 | } 226 | }, 227 | 228 | dataInitial: function (sm, chunk) { 229 | var remaining = this.initialPayloadRemaining - chunk.length; 230 | if (remaining > 0) { 231 | this.initialPayloadRemaining = remaining; 232 | sm.switchTo('dataInitial'); 233 | } else { 234 | this.initialPayloadRemaining = this.initialPayloadLength; 235 | if (remaining < 0) { 236 | (sm.stepper())(sm, chunk.slice(this.initialPayloadRemaining)); 237 | } 238 | } 239 | }, 240 | 241 | dataOpen: function (sm, chunk) { 242 | var fsm; 243 | chunk = this.partialChunk.concat(chunk); 244 | if (chunk.length < 2) { 245 | this.partialChunk = chunk; 246 | sm.switchTo('dataOpen'); 247 | } else { 248 | this.partialChunk = ""; 249 | if ('o\n' === chunk.slice(0, 2)) { 250 | fsm = {}; 251 | util.shallowCopy(this.fsm, fsm); 252 | this.fsm = fsm; 253 | fsm['dataInitial'] = 'running'; // from here on, another 'o\n' is an error 254 | this.emit('connection', this); 255 | if (2 < chunk.length) { 256 | (sm.stepper())(sm, chunk.slice(2)); 257 | } 258 | } else { 259 | this.error(sm, chunk); 260 | } 261 | } 262 | }, 263 | 264 | running: function (sm, chunk) { 265 | var type; 266 | chunk = this.partialChunk.concat(chunk); 267 | if (1 < chunk.length) { 268 | type = chunk.charAt(0); 269 | switch (type) { 270 | case 'h': // heartbeat 271 | this.partialChunk = chunk.slice(2); 272 | break; 273 | case 'a': // data 274 | this.emitData(chunk, this.partialChunk.length); 275 | break; 276 | case 'c': // close frame 277 | this.close(); 278 | break; 279 | default: 280 | this.error(sm, "Unexpected frame type", type, chunk); 281 | } 282 | } else { 283 | this.partialChunk = chunk; 284 | } 285 | }, 286 | 287 | emitData: function (chunk, searchStart) { 288 | var index = chunk.indexOf('\n', searchStart), 289 | array, i; 290 | if (-1 === index) { 291 | this.partialChunk = chunk; 292 | } else { 293 | index += 1; 294 | if (index === chunk.length) { 295 | this.partialChunk = ""; 296 | } else { 297 | this.partialChunk = chunk.slice(index); 298 | } 299 | array = JSON.parse(chunk.slice(1, index)); 300 | for (i = 0; i < array.length; i += 1) { 301 | this.sjs.emit('data', array[i]); 302 | } 303 | } 304 | }, 305 | 306 | reopen: function (sm) { 307 | (sm.stepper('start'))(); 308 | }, 309 | 310 | error: function () { 311 | if (undefined !== this.response) { 312 | this.response.removeAllListeners(); 313 | this.response.destroy(); 314 | } 315 | var args = Array.prototype.slice.call(arguments, 0), 316 | sm; 317 | sm = args.shift(); 318 | sm.switchTo('errored'); 319 | this.emit('error', args); 320 | }, 321 | 322 | errored: function () {} 323 | } 324 | 325 | function StateMachine (callbacks) { 326 | this.callbacks = callbacks; 327 | this.stepper = Object.getPrototypeOf(this).stepper.bind(this); 328 | this.fun = this.stepper(); 329 | } 330 | StateMachine.prototype = { 331 | invoke: function () { 332 | if (undefined === this.fun) { 333 | throw new InvalidState(this); 334 | } 335 | var args = Array.prototype.slice.call(arguments, 0); 336 | args.unshift(this); 337 | return this.fun.apply(this.callbacks, args); 338 | }, 339 | 340 | nextStateName: function () { 341 | if (util.hasOwnProperty(this, 'switchedTo')) { 342 | return this.switchedTo; 343 | } else if (util.hasOwnProperty(this, 'stateName')) { 344 | return this.callbacks.fsm[this.stateName]; 345 | } else { 346 | return 'start'; 347 | } 348 | }, 349 | 350 | switchTo: function (name) { 351 | if (undefined === name) { 352 | delete this.switchedTo; 353 | } else { 354 | this.switchedTo = name; 355 | } 356 | }, 357 | 358 | stepper: function (name) { 359 | return (function () { 360 | if (undefined !== name) { 361 | this.switchTo(name); 362 | } 363 | this.stateName = this.nextStateName(); 364 | this.switchTo(); 365 | this.fun = this.callbacks[this.stateName]; 366 | this.invoke.apply(this, arguments); 367 | }).bind(this); 368 | } 369 | }; 370 | 371 | exports.create = function (url) { 372 | var sjsc = new SockJSClient(url); 373 | sjsc.connect(); 374 | return sjsc; 375 | }; 376 | exports.InvalidURL = InvalidURL; 377 | exports.InvalidState = InvalidState; 378 | 379 | }(this)); 380 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs-client", 3 | "author": "Matthew Sackman", 4 | "version": "0.1.3", 5 | "keywords": ["websockets", "websocket"], 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/sockjs/sockjs-client-node.git" 9 | }, 10 | "main": "index", 11 | "description": "Client library for SockJS", 12 | "dependencies": { 13 | "node-uuid": "1.3.3" 14 | }, 15 | "license": "MIT" 16 | } 17 | --------------------------------------------------------------------------------