1 // ========================================================================== 2 // Project: SC.WebSocket 3 // Copyright: ©2013 Nicolas BADIA and contributors 4 // License: Licensed under MIT license (see license.js) 5 // ========================================================================== 6 7 sc_require('mixins/websocket_delegate'); 8 9 /** 10 @class 11 12 Implements SproutCore run loop aware event handling for WebSocket. Using SC.WebSocket ensures 13 that the run loop runs on each WebSocket event and provides a useful skeleton for handling 14 WebSocket events. 15 16 Example Usage: 17 18 // Create a WebSocket connection. 19 var ws = SC.WebSocket.create({ 20 server: 'ws://server', 21 }); 22 23 // Assign target and methods for events. 24 ws.notify('onopen', this, 'wsOpened'); 25 ws.notify('onmessage', this, 'wsReceivedMessage'); 26 ws.notify('onclose', this, 'wsClosed'); 27 ws.connect(); 28 29 // Send a message through the WebSocket. 30 ws.send('hello server'); 31 32 @since SproutCore 1.11 33 @extends SC.Object 34 @extends SC.DelegateSupport 35 @author Nicolas BADIA 36 */ 37 SC.WebSocket = SC.Object.extend(SC.DelegateSupport, SC.WebSocketDelegate, 38 /** @scope SC.WebSocket.prototype */ { 39 40 /** 41 The URL of the WebSocket server. 42 43 @type String 44 @default null 45 */ 46 server: null, 47 48 /** 49 Whether the connection is open or not. 50 51 @type Boolean 52 @readOnly 53 */ 54 isConnected: false, 55 56 /** 57 In order to handle authentication, set `isAuth` to `false` in the 58 `webSocketDidOpen` delegate method just after sending a request to 59 authenticate the connection. This way, any futher messages will be put in the 60 queue until the server tells you that the connection is authenticated. Once it 61 is, you should set `isAuth` to `true` to resume the queue. 62 63 If you don't need authentication, leave `isAuth` as `null`. 64 65 @type Boolean 66 @default null 67 */ 68 isAuth: null, 69 70 /** 71 Processes the messages as JSON if possible. 72 73 @type Boolean 74 @default true 75 */ 76 isJSON: true, 77 78 /** 79 A WebSocket delegate. 80 81 @see SC.WebSocketDelegate 82 @type SC.WebSocketDelegate 83 @default null 84 */ 85 delegate: null, 86 87 /** 88 Whether to attempt to reconnect automatically if the connection is closed or not. 89 90 @type SC.WebSocketDelegate 91 @default true 92 */ 93 autoReconnect: true, 94 95 /** 96 The interval in milliseconds to wait before trying to reconnect. 97 98 @type SC.WebSocketDelegate 99 @default null 100 */ 101 reconnectInterval: 10000, // 10 seconds 102 103 // .......................................................... 104 // PUBLIC METHODS 105 // 106 107 /** 108 Open the WebSocket connection. 109 110 @returns {SC.WebSocket} The SC.WebSocket object. 111 */ 112 connect: function() { 113 // If not supported or already connected, return. 114 if (!SC.platform.supportsWebSocket || this.socket) return this; 115 116 // Connect. 117 try { 118 var socket = this.socket = new WebSocket(this.get('server')), 119 self = this; 120 121 socket.onopen = function (open) { 122 SC.run(function () { 123 self.onOpen(open); 124 }); 125 }; 126 127 socket.onmessage = function (message) { 128 SC.run(function () { 129 self.onMessage(message); 130 }); 131 }; 132 133 socket.onclose = function (close) { 134 SC.run(function () { 135 self.onClose(close); 136 }); 137 }; 138 139 socket.onerror = function (error) { 140 SC.run(function () { 141 self.onError(error); 142 }); 143 }; 144 } catch (e) { 145 SC.error('An error has occurred while connnecting to the websocket server: ' + e); 146 } 147 148 return this; 149 }, 150 151 /** 152 Close the connection. 153 154 @param {Number} code A numeric value indicating the status code explaining why the connection is being closed. If this parameter is not specified, a default value of 1000 (indicating a normal "transaction complete" closure) is assumed. 155 @param {String} reason A human-readable string explaining why the connection is closing. This string must be no longer than 123 bytes of UTF-8 text (not characters). 156 @returns {SC.WebSocket} The SC.WebSocket object. 157 */ 158 close: function(code, reason) { 159 var socket = this.socket; 160 161 if (socket && socket.readyState === SC.WebSocket.OPEN) { 162 this.socket.close(code, reason); 163 } 164 165 return this; 166 }, 167 168 /** 169 Configures a callback to execute when an event happens. You must pass at least a target and 170 method to this and optionally an event name. 171 172 You may also pass additional arguments which will then be passed along to your callback. 173 174 Example: 175 176 var websocket = SC.WebSocket.create({ server: 'ws://server' }).connect(); 177 178 webSocket.notify('onopen', this, 'wsWasOpened'); 179 webSocket.notify('onmessage', this, 'wsReceivedMessage'); 180 webSocket.notify('onclose', this, 'wsWasClose'); 181 webSocket.notify('onerror', this, 'wsDidError'); 182 183 ## Callback Format 184 185 Your notification callback should expect to receive the WebSocket object as 186 the first parameter and the event or message; plus any additional parameters that you pass. If your callback handles the notification and to prevent further handling, it 187 should return YES. 188 189 @param {String} target String Event name. 190 @param {Object} target The target object for the callback action. 191 @param {String|Function} action The method name or function to call on the target. 192 @returns {SC.WebSocket} The SC.WebSocket object. 193 */ 194 notify: function(event, target, action) { 195 var args, 196 i, len; 197 198 if (SC.typeOf(event) !== SC.T_STRING) { 199 // Fast arguments access. 200 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 201 args = new Array(Math.max(0, arguments.length - 2)); // SC.A(arguments).slice(2) 202 for (i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 2]; } 203 204 // Shift the arguments 205 action = target; 206 target = event; 207 event = 'onmessage'; 208 } else { 209 // Fast arguments access. 210 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 211 args = new Array(Math.max(0, arguments.length - 3)); // SC.A(arguments).slice(3) 212 for (i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 3]; } 213 } 214 215 var listeners = this.get('listeners'); 216 if (!listeners) { this.set('listeners', listeners = {}); } 217 if(!listeners[event]) { listeners[event] = []; } 218 219 //@if(debug) 220 for (i = listeners[event].length - 1; i >= 0; i--) { 221 var listener = listeners[event][i]; 222 if (listener.event === event && listener.target === target && listener.action === action) { 223 SC.warn("Developer Warning: This listener is already defined."); 224 } 225 } 226 //@endif 227 228 // Add another listener for the given event name. 229 listeners[event].push({target: target, action: action, args: args}); 230 231 return this; 232 }, 233 234 /** 235 Send the message on the WebSocket. If the connection is not yet open or authenticated (as 236 necessary), the message will be put in the queue. 237 238 If `isJSON` is true (the default for SC.WebSocket), the message will be stringified JSON. 239 240 @param {String|Object} message The message to send. 241 @returns {SC.WebSocket} 242 */ 243 send: function(message) { 244 if (this.isConnected === true && this.isAuth !== false) { 245 if (this.isJSON) { 246 message = JSON.stringify(message); 247 } 248 249 this.socket.send(message); 250 } else { 251 this.addToQueue(message); 252 } 253 return this; 254 }, 255 256 // .......................................................... 257 // PRIVATE METHODS 258 // 259 260 /** 261 @private 262 */ 263 onOpen: function(event) { 264 var del = this.get('objectDelegate'); 265 266 this.set('isConnected', true); 267 268 var ret = del.webSocketDidOpen(this, event); 269 if (ret !== true) this._notifyListeners('onopen', event); 270 271 this.fireQueue(); 272 }, 273 274 /** 275 @private 276 */ 277 onMessage: function(messageEvent) { 278 if (messageEvent) { 279 var del = this.get('objectDelegate'), 280 message, 281 data, 282 ret; 283 284 message = data = messageEvent.data; 285 ret = del.webSocketDidReceiveMessage(this, data); 286 287 if (ret !== true) { 288 if (this.isJSON) { 289 message = JSON.parse(data); 290 } 291 this._notifyListeners('onmessage', message); 292 } 293 } 294 295 // If there is message in the queue, we fire them 296 this.fireQueue(); 297 }, 298 299 /** 300 @private 301 */ 302 onClose: function(closeEvent) { 303 var del = this.get('objectDelegate'); 304 305 this.set('isConnected', false); 306 this.set('isAuth', null); 307 this.socket = null; 308 309 var ret = del.webSocketDidClose(this, closeEvent); 310 311 if (ret !== true) { 312 this._notifyListeners('onclose', closeEvent); 313 this.tryReconnect(); 314 } 315 }, 316 317 /** 318 @private 319 */ 320 onError: function(event) { 321 var del = this.get('objectDelegate'), 322 ret = del.webSocketDidError(this, event); 323 324 if (ret !== true) this._notifyListeners('onerror', event); 325 }, 326 327 /** 328 @private 329 330 Add the message to the queue 331 */ 332 addToQueue: function(message) { 333 var queue = this.queue; 334 if (!queue) { this.queue = queue = []; } 335 336 queue.push(message); 337 }, 338 339 /** 340 @private 341 342 Send the messages from the queue. 343 */ 344 fireQueue: function() { 345 var queue = this.queue; 346 if (!queue || queue.length === 0) return; 347 348 queue = SC.A(queue); 349 this.queue = null; 350 351 for (var i = 0, len = queue.length; i < len; i++) { 352 var message = queue[i]; 353 this.send(message); 354 } 355 }, 356 357 /** 358 @private 359 */ 360 tryReconnect: function() { 361 if (!this.get('autoReconnect')) return; 362 363 var that = this; 364 setTimeout(function() { that.connect(); }, this.get('reconnectInterval')); 365 }, 366 367 /** 368 @private 369 370 Will notify each listener. Returns true if any of the listeners handle. 371 */ 372 _notifyListeners: function(event, message) { 373 var listeners = (this.listeners || {})[event], notifier, target, action, args; 374 if (!listeners) { return NO; } 375 376 var handled = NO, 377 len = listeners.length; 378 379 for (var i = 0; i < len; i++) { 380 notifier = listeners[i]; 381 args = (notifier.args || []).copy(); 382 args.unshift(message); 383 args.unshift(this); 384 385 target = notifier.target; 386 action = notifier.action; 387 if (SC.typeOf(action) === SC.T_STRING) { action = target[action]; } 388 389 handled = action.apply(target, args); 390 if (handled === true) return handled; 391 } 392 393 return handled; 394 }, 395 396 /** 397 @private 398 */ 399 objectDelegate: function () { 400 var del = this.get('delegate'); 401 return this.delegateFor('isWebSocketDelegate', del, this); 402 }.property('delegate').cacheable(), 403 404 // .......................................................... 405 // PRIVATE PROPERTIES 406 // 407 408 /** 409 @private 410 411 @type WebSocket 412 @default null 413 */ 414 socket: null, 415 416 /** 417 @private 418 419 @type Object 420 @default null 421 */ 422 listeners: null, 423 424 /** 425 @private 426 427 Messages that needs to be send once the connection is open. 428 429 @type Array 430 @default null 431 */ 432 queue: null, 433 434 }); 435 436 // Class Methods 437 SC.WebSocket.mixin( /** @scope SC.WebSocket */ { 438 439 // .......................................................... 440 // CONSTANTS 441 // 442 443 /** 444 The connection is not yet open. 445 446 @static 447 @constant 448 @type Number 449 @default 0 450 */ 451 CONNECTING: 0, 452 453 /** 454 The connection is open and ready to communicate. 455 456 @static 457 @constant 458 @type Number 459 @default 1 460 */ 461 OPEN: 1, 462 463 /** 464 The connection is in the process of closing. 465 466 @static 467 @constant 468 @type Number 469 @default 2 470 */ 471 CLOSING: 2, 472 473 /** 474 The connection is closed or couldn't be opened. 475 476 @static 477 @constant 478 @type Number 479 @default 3 480 */ 481 CLOSED: 3, 482 483 }); 484