////////////////////////////////////////////////////////////////////////// // // // This is a generated file. You can view the original // // source in your browser if your browser supports source maps. // // // // If you are using Chrome, open the Developer Tools and click the gear // // icon in its lower right corner. In the General Settings panel, turn // // on 'Enable source maps'. // // // // If you are using Firefox 23, go to `about:config` and set the // // `devtools.debugger.source-maps-enabled` preference to true. // // (The preference should be on by default in Firefox 24; versions // // older than 23 do not support source maps.) // // // ////////////////////////////////////////////////////////////////////////// (function () { /* Imports */ var Meteor = Package.meteor.Meteor; var check = Package.check.check; var Match = Package.check.Match; var Random = Package.random.Random; var EJSON = Package.ejson.EJSON; var JSON = Package.json.JSON; var _ = Package.underscore._; var Tracker = Package.tracker.Tracker; var Deps = Package.tracker.Deps; var Log = Package.logging.Log; var Retry = Package.retry.Retry; var LocalCollection = Package.minimongo.LocalCollection; var Minimongo = Package.minimongo.Minimongo; /* Package-scope variables */ var DDP, LivedataTest, SockJS, toSockjsUrl, toWebsocketUrl, Heartbeat, SUPPORTED_DDP_VERSIONS, MethodInvocation, parseDDP, stringifyDDP, RandomStream, makeRpcSeed, allConnections; (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/common.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /** // 1 * @namespace DDP // 2 * @summary The namespace for DDP-related methods. // 3 */ // 4 DDP = {}; // 5 LivedataTest = {}; // 6 // 7 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/sockjs-0.3.4.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // XXX METEOR changes in <METEOR> // 1 // 2 /* SockJS client, version 0.3.4, http://sockjs.org, MIT License // 3 // 4 Copyright (c) 2011-2012 VMware, Inc. // 5 // 6 Permission is hereby granted, free of charge, to any person obtaining a copy // 7 of this software and associated documentation files (the "Software"), to deal // 8 in the Software without restriction, including without limitation the rights // 9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // 10 copies of the Software, and to permit persons to whom the Software is // 11 furnished to do so, subject to the following conditions: // 12 // 13 The above copyright notice and this permission notice shall be included in // 14 all copies or substantial portions of the Software. // 15 // 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // 22 THE SOFTWARE. // 23 */ // 24 // 25 // <METEOR> Commented out JSO implementation (use json package instead). // 26 // JSON2 by Douglas Crockford (minified). // 27 // var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c<f;c+=1)h[c]=str(c,i)||"null";e=h.length===0?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g;return e}if(rep&&typeof rep=="object"){f=rep.length;for(c=0;c<f;c+=1)typeof rep[c]=="string"&&(d=rep[c],e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e))}else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e));e=h.length===0?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g;return e}}function quote(a){escapable.lastIndex=0;return escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function f(a){return a<10?"0"+a:a}"use strict",typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(a){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()});var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(a,b,c){var d;gap="",indent="";if(typeof c=="number")for(d=0;d<c;d+=1)indent+=" ";else typeof c=="string"&&(indent=c);rep=b;if(!b||typeof b=="function"||typeof b=="object"&&typeof b.length=="number")return str("",{"":a});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),d!==undefined?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""))){j=eval("("+text+")");return typeof reviver=="function"?walk({"":j},""):j}throw new SyntaxError("JSON.parse")})}() // </METEOR> // 29 // 30 // [*] Including lib/index.js // 31 // Public object // 32 SockJS = (function(){ // 33 var _document = document; // 34 var _window = window; // 35 var utils = {}; // 36 // 37 // 38 // [*] Including lib/reventtarget.js // 39 /* // 40 * ***** BEGIN LICENSE BLOCK ***** // 41 * Copyright (c) 2011-2012 VMware, Inc. // 42 * // 43 * For the license see COPYING. // 44 * ***** END LICENSE BLOCK ***** // 45 */ // 46 // 47 /* Simplified implementation of DOM2 EventTarget. // 48 * http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget // 49 */ // 50 var REventTarget = function() {}; // 51 REventTarget.prototype.addEventListener = function (eventType, listener) { // 52 if(!this._listeners) { // 53 this._listeners = {}; // 54 } // 55 if(!(eventType in this._listeners)) { // 56 this._listeners[eventType] = []; // 57 } // 58 var arr = this._listeners[eventType]; // 59 if(utils.arrIndexOf(arr, listener) === -1) { // 60 arr.push(listener); // 61 } // 62 return; // 63 }; // 64 // 65 REventTarget.prototype.removeEventListener = function (eventType, listener) { // 66 if(!(this._listeners && (eventType in this._listeners))) { // 67 return; // 68 } // 69 var arr = this._listeners[eventType]; // 70 var idx = utils.arrIndexOf(arr, listener); // 71 if (idx !== -1) { // 72 if(arr.length > 1) { // 73 this._listeners[eventType] = arr.slice(0, idx).concat( arr.slice(idx+1) ); // 74 } else { // 75 delete this._listeners[eventType]; // 76 } // 77 return; // 78 } // 79 return; // 80 }; // 81 // 82 REventTarget.prototype.dispatchEvent = function (event) { // 83 var t = event.type; // 84 var args = Array.prototype.slice.call(arguments, 0); // 85 if (this['on'+t]) { // 86 this['on'+t].apply(this, args); // 87 } // 88 if (this._listeners && t in this._listeners) { // 89 for(var i=0; i < this._listeners[t].length; i++) { // 90 this._listeners[t][i].apply(this, args); // 91 } // 92 } // 93 }; // 94 // [*] End of lib/reventtarget.js // 95 // 96 // 97 // [*] Including lib/simpleevent.js // 98 /* // 99 * ***** BEGIN LICENSE BLOCK ***** // 100 * Copyright (c) 2011-2012 VMware, Inc. // 101 * // 102 * For the license see COPYING. // 103 * ***** END LICENSE BLOCK ***** // 104 */ // 105 // 106 var SimpleEvent = function(type, obj) { // 107 this.type = type; // 108 if (typeof obj !== 'undefined') { // 109 for(var k in obj) { // 110 if (!obj.hasOwnProperty(k)) continue; // 111 this[k] = obj[k]; // 112 } // 113 } // 114 }; // 115 // 116 SimpleEvent.prototype.toString = function() { // 117 var r = []; // 118 for(var k in this) { // 119 if (!this.hasOwnProperty(k)) continue; // 120 var v = this[k]; // 121 if (typeof v === 'function') v = '[function]'; // 122 r.push(k + '=' + v); // 123 } // 124 return 'SimpleEvent(' + r.join(', ') + ')'; // 125 }; // 126 // [*] End of lib/simpleevent.js // 127 // 128 // 129 // [*] Including lib/eventemitter.js // 130 /* // 131 * ***** BEGIN LICENSE BLOCK ***** // 132 * Copyright (c) 2011-2012 VMware, Inc. // 133 * // 134 * For the license see COPYING. // 135 * ***** END LICENSE BLOCK ***** // 136 */ // 137 // 138 var EventEmitter = function(events) { // 139 var that = this; // 140 that._events = events || []; // 141 that._listeners = {}; // 142 }; // 143 EventEmitter.prototype.emit = function(type) { // 144 var that = this; // 145 that._verifyType(type); // 146 if (that._nuked) return; // 147 // 148 var args = Array.prototype.slice.call(arguments, 1); // 149 if (that['on'+type]) { // 150 that['on'+type].apply(that, args); // 151 } // 152 if (type in that._listeners) { // 153 for(var i = 0; i < that._listeners[type].length; i++) { // 154 that._listeners[type][i].apply(that, args); // 155 } // 156 } // 157 }; // 158 // 159 EventEmitter.prototype.on = function(type, callback) { // 160 var that = this; // 161 that._verifyType(type); // 162 if (that._nuked) return; // 163 // 164 if (!(type in that._listeners)) { // 165 that._listeners[type] = []; // 166 } // 167 that._listeners[type].push(callback); // 168 }; // 169 // 170 EventEmitter.prototype._verifyType = function(type) { // 171 var that = this; // 172 if (utils.arrIndexOf(that._events, type) === -1) { // 173 utils.log('Event ' + JSON.stringify(type) + // 174 ' not listed ' + JSON.stringify(that._events) + // 175 ' in ' + that); // 176 } // 177 }; // 178 // 179 EventEmitter.prototype.nuke = function() { // 180 var that = this; // 181 that._nuked = true; // 182 for(var i=0; i<that._events.length; i++) { // 183 delete that[that._events[i]]; // 184 } // 185 that._listeners = {}; // 186 }; // 187 // [*] End of lib/eventemitter.js // 188 // 189 // 190 // [*] Including lib/utils.js // 191 /* // 192 * ***** BEGIN LICENSE BLOCK ***** // 193 * Copyright (c) 2011-2012 VMware, Inc. // 194 * // 195 * For the license see COPYING. // 196 * ***** END LICENSE BLOCK ***** // 197 */ // 198 // 199 var random_string_chars = 'abcdefghijklmnopqrstuvwxyz0123456789_'; // 200 utils.random_string = function(length, max) { // 201 max = max || random_string_chars.length; // 202 var i, ret = []; // 203 for(i=0; i < length; i++) { // 204 ret.push( random_string_chars.substr(Math.floor(Math.random() * max),1) ); // 205 } // 206 return ret.join(''); // 207 }; // 208 utils.random_number = function(max) { // 209 return Math.floor(Math.random() * max); // 210 }; // 211 utils.random_number_string = function(max) { // 212 var t = (''+(max - 1)).length; // 213 var p = Array(t+1).join('0'); // 214 return (p + utils.random_number(max)).slice(-t); // 215 }; // 216 // 217 // Assuming that url looks like: http://asdasd:111/asd // 218 utils.getOrigin = function(url) { // 219 url += '/'; // 220 var parts = url.split('/').slice(0, 3); // 221 return parts.join('/'); // 222 }; // 223 // 224 utils.isSameOriginUrl = function(url_a, url_b) { // 225 // location.origin would do, but it's not always available. // 226 if (!url_b) url_b = _window.location.href; // 227 // 228 return (url_a.split('/').slice(0,3).join('/') // 229 === // 230 url_b.split('/').slice(0,3).join('/')); // 231 }; // 232 // 233 // <METEOR> // 234 // https://github.com/sockjs/sockjs-client/issues/79 // 235 utils.isSameOriginScheme = function(url_a, url_b) { // 236 if (!url_b) url_b = _window.location.href; // 237 // 238 return (url_a.split(':')[0] // 239 === // 240 url_b.split(':')[0]); // 241 }; // 242 // </METEOR> // 243 // 244 // 245 utils.getParentDomain = function(url) { // 246 // ipv4 ip address // 247 if (/^[0-9.]*$/.test(url)) return url; // 248 // ipv6 ip address // 249 if (/^\[/.test(url)) return url; // 250 // no dots // 251 if (!(/[.]/.test(url))) return url; // 252 // 253 var parts = url.split('.').slice(1); // 254 return parts.join('.'); // 255 }; // 256 // 257 utils.objectExtend = function(dst, src) { // 258 for(var k in src) { // 259 if (src.hasOwnProperty(k)) { // 260 dst[k] = src[k]; // 261 } // 262 } // 263 return dst; // 264 }; // 265 // 266 var WPrefix = '_jp'; // 267 // 268 utils.polluteGlobalNamespace = function() { // 269 if (!(WPrefix in _window)) { // 270 _window[WPrefix] = {}; // 271 } // 272 }; // 273 // 274 utils.closeFrame = function (code, reason) { // 275 return 'c'+JSON.stringify([code, reason]); // 276 }; // 277 // 278 utils.userSetCode = function (code) { // 279 return code === 1000 || (code >= 3000 && code <= 4999); // 280 }; // 281 // 282 // See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/ // 283 // and RFC 2988. // 284 utils.countRTO = function (rtt) { // 285 var rto; // 286 if (rtt > 100) { // 287 rto = 3 * rtt; // rto > 300msec // 288 } else { // 289 rto = rtt + 200; // 200msec < rto <= 300msec // 290 } // 291 return rto; // 292 } // 293 // 294 utils.log = function() { // 295 if (_window.console && console.log && console.log.apply) { // 296 console.log.apply(console, arguments); // 297 } // 298 }; // 299 // 300 utils.bind = function(fun, that) { // 301 if (fun.bind) { // 302 return fun.bind(that); // 303 } else { // 304 return function() { // 305 return fun.apply(that, arguments); // 306 }; // 307 } // 308 }; // 309 // 310 utils.flatUrl = function(url) { // 311 return url.indexOf('?') === -1 && url.indexOf('#') === -1; // 312 }; // 313 // 314 // `relativeTo` is an optional absolute URL. If provided, `url` will be // 315 // interpreted relative to `relativeTo`. Defaults to `document.location`. // 316 // <METEOR> // 317 utils.amendUrl = function(url, relativeTo) { // 318 var baseUrl; // 319 if (relativeTo === undefined) { // 320 baseUrl = _document.location; // 321 } else { // 322 var protocolMatch = /^([a-z0-9.+-]+:)/i.exec(relativeTo); // 323 if (protocolMatch) { // 324 var protocol = protocolMatch[0].toLowerCase(); // 325 var rest = relativeTo.substring(protocol.length); // 326 var hostMatch = /[a-z0-9\.-]+(:[0-9]+)?/.exec(rest); // 327 if (hostMatch) // 328 var host = hostMatch[0]; // 329 } // 330 if (! protocol || ! host) // 331 throw new Error("relativeTo must be an absolute url"); // 332 baseUrl = { // 333 protocol: protocol, // 334 host: host // 335 }; // 336 } // 337 if (!url) { // 338 throw new Error('Wrong url for SockJS'); // 339 } // 340 if (!utils.flatUrl(url)) { // 341 throw new Error('Only basic urls are supported in SockJS'); // 342 } // 343 // 344 // '//abc' --> 'http://abc' // 345 if (url.indexOf('//') === 0) { // 346 url = baseUrl.protocol + url; // 347 } // 348 // '/abc' --> 'http://localhost:1234/abc' // 349 if (url.indexOf('/') === 0) { // 350 url = baseUrl.protocol + '//' + baseUrl.host + url; // 351 } // 352 // </METEOR> // 353 // strip trailing slashes // 354 url = url.replace(/[/]+$/,''); // 355 // 356 // We have a full url here, with proto and host. For some browsers // 357 // http://localhost:80/ is not in the same origin as http://localhost/ // 358 // Remove explicit :80 or :443 in such cases. See #74 // 359 var parts = url.split("/"); // 360 if ((parts[0] === "http:" && /:80$/.test(parts[2])) || // 361 (parts[0] === "https:" && /:443$/.test(parts[2]))) { // 362 parts[2] = parts[2].replace(/:(80|443)$/, ""); // 363 } // 364 url = parts.join("/"); // 365 return url; // 366 }; // 367 // 368 // IE doesn't support [].indexOf. // 369 utils.arrIndexOf = function(arr, obj){ // 370 for(var i=0; i < arr.length; i++){ // 371 if(arr[i] === obj){ // 372 return i; // 373 } // 374 } // 375 return -1; // 376 }; // 377 // 378 utils.arrSkip = function(arr, obj) { // 379 var idx = utils.arrIndexOf(arr, obj); // 380 if (idx === -1) { // 381 return arr.slice(); // 382 } else { // 383 var dst = arr.slice(0, idx); // 384 return dst.concat(arr.slice(idx+1)); // 385 } // 386 }; // 387 // 388 // Via: https://gist.github.com/1133122/2121c601c5549155483f50be3da5305e83b8c5df // 389 utils.isArray = Array.isArray || function(value) { // 390 return {}.toString.call(value).indexOf('Array') >= 0 // 391 }; // 392 // 393 utils.delay = function(t, fun) { // 394 if(typeof t === 'function') { // 395 fun = t; // 396 t = 0; // 397 } // 398 return setTimeout(fun, t); // 399 }; // 400 // 401 // 402 // Chars worth escaping, as defined by Douglas Crockford: // 403 // https://github.com/douglascrockford/JSON-js/blob/47a9882cddeb1e8529e07af9736218075372b8ac/json2.js#L196 // 404 var json_escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, json_lookup = { // 406 "\u0000":"\\u0000","\u0001":"\\u0001","\u0002":"\\u0002","\u0003":"\\u0003", // 407 "\u0004":"\\u0004","\u0005":"\\u0005","\u0006":"\\u0006","\u0007":"\\u0007", // 408 "\b":"\\b","\t":"\\t","\n":"\\n","\u000b":"\\u000b","\f":"\\f","\r":"\\r", // 409 "\u000e":"\\u000e","\u000f":"\\u000f","\u0010":"\\u0010","\u0011":"\\u0011", // 410 "\u0012":"\\u0012","\u0013":"\\u0013","\u0014":"\\u0014","\u0015":"\\u0015", // 411 "\u0016":"\\u0016","\u0017":"\\u0017","\u0018":"\\u0018","\u0019":"\\u0019", // 412 "\u001a":"\\u001a","\u001b":"\\u001b","\u001c":"\\u001c","\u001d":"\\u001d", // 413 "\u001e":"\\u001e","\u001f":"\\u001f","\"":"\\\"","\\":"\\\\", // 414 "\u007f":"\\u007f","\u0080":"\\u0080","\u0081":"\\u0081","\u0082":"\\u0082", // 415 "\u0083":"\\u0083","\u0084":"\\u0084","\u0085":"\\u0085","\u0086":"\\u0086", // 416 "\u0087":"\\u0087","\u0088":"\\u0088","\u0089":"\\u0089","\u008a":"\\u008a", // 417 "\u008b":"\\u008b","\u008c":"\\u008c","\u008d":"\\u008d","\u008e":"\\u008e", // 418 "\u008f":"\\u008f","\u0090":"\\u0090","\u0091":"\\u0091","\u0092":"\\u0092", // 419 "\u0093":"\\u0093","\u0094":"\\u0094","\u0095":"\\u0095","\u0096":"\\u0096", // 420 "\u0097":"\\u0097","\u0098":"\\u0098","\u0099":"\\u0099","\u009a":"\\u009a", // 421 "\u009b":"\\u009b","\u009c":"\\u009c","\u009d":"\\u009d","\u009e":"\\u009e", // 422 "\u009f":"\\u009f","\u00ad":"\\u00ad","\u0600":"\\u0600","\u0601":"\\u0601", // 423 "\u0602":"\\u0602","\u0603":"\\u0603","\u0604":"\\u0604","\u070f":"\\u070f", // 424 "\u17b4":"\\u17b4","\u17b5":"\\u17b5","\u200c":"\\u200c","\u200d":"\\u200d", // 425 "\u200e":"\\u200e","\u200f":"\\u200f","\u2028":"\\u2028","\u2029":"\\u2029", // 426 "\u202a":"\\u202a","\u202b":"\\u202b","\u202c":"\\u202c","\u202d":"\\u202d", // 427 "\u202e":"\\u202e","\u202f":"\\u202f","\u2060":"\\u2060","\u2061":"\\u2061", // 428 "\u2062":"\\u2062","\u2063":"\\u2063","\u2064":"\\u2064","\u2065":"\\u2065", // 429 "\u2066":"\\u2066","\u2067":"\\u2067","\u2068":"\\u2068","\u2069":"\\u2069", // 430 "\u206a":"\\u206a","\u206b":"\\u206b","\u206c":"\\u206c","\u206d":"\\u206d", // 431 "\u206e":"\\u206e","\u206f":"\\u206f","\ufeff":"\\ufeff","\ufff0":"\\ufff0", // 432 "\ufff1":"\\ufff1","\ufff2":"\\ufff2","\ufff3":"\\ufff3","\ufff4":"\\ufff4", // 433 "\ufff5":"\\ufff5","\ufff6":"\\ufff6","\ufff7":"\\ufff7","\ufff8":"\\ufff8", // 434 "\ufff9":"\\ufff9","\ufffa":"\\ufffa","\ufffb":"\\ufffb","\ufffc":"\\ufffc", // 435 "\ufffd":"\\ufffd","\ufffe":"\\ufffe","\uffff":"\\uffff"}; // 436 // 437 // Some extra characters that Chrome gets wrong, and substitutes with // 438 // something else on the wire. // 439 var extra_escapable = /[\x00-\x1f\ud800-\udfff\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0f80-\u0f83\u0f93\u0f9d\u0fa2\u0fa7\u0fac\u0fb9\u1939-\u193a\u1a17\u1b6b\u1cda-\u1cdb\u1dc0-\u1dcf\u1dfc\u1dfe\u1f71\u1f73\u1f75\u1f77\u1f79\u1f7b\u1f7d\u1fbb\u1fbe\u1fc9\u1fcb\u1fd3\u1fdb\u1fe3\u1feb\u1fee-\u1fef\u1ff9\u1ffb\u1ffd\u2000-\u2001\u20d0-\u20d1\u20d4-\u20d7\u20e7-\u20e9\u2126\u212a-\u212b\u2329-\u232a\u2adc\u302b-\u302c\uaab2-\uaab3\uf900-\ufa0d\ufa10\ufa12\ufa15-\ufa1e\ufa20\ufa22\ufa25-\ufa26\ufa2a-\ufa2d\ufa30-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4e\ufff0-\uffff]/g, extra_lookup; // 441 // 442 // JSON Quote string. Use native implementation when possible. // 443 var JSONQuote = (JSON && JSON.stringify) || function(string) { // 444 json_escapable.lastIndex = 0; // 445 if (json_escapable.test(string)) { // 446 string = string.replace(json_escapable, function(a) { // 447 return json_lookup[a]; // 448 }); // 449 } // 450 return '"' + string + '"'; // 451 }; // 452 // 453 // This may be quite slow, so let's delay until user actually uses bad // 454 // characters. // 455 var unroll_lookup = function(escapable) { // 456 var i; // 457 var unrolled = {} // 458 var c = [] // 459 for(i=0; i<65536; i++) { // 460 c.push( String.fromCharCode(i) ); // 461 } // 462 escapable.lastIndex = 0; // 463 c.join('').replace(escapable, function (a) { // 464 unrolled[ a ] = '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); // 465 return ''; // 466 }); // 467 escapable.lastIndex = 0; // 468 return unrolled; // 469 }; // 470 // 471 // Quote string, also taking care of unicode characters that browsers // 472 // often break. Especially, take care of unicode surrogates: // 473 // http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates // 474 utils.quote = function(string) { // 475 var quoted = JSONQuote(string); // 476 // 477 // In most cases this should be very fast and good enough. // 478 extra_escapable.lastIndex = 0; // 479 if(!extra_escapable.test(quoted)) { // 480 return quoted; // 481 } // 482 // 483 if(!extra_lookup) extra_lookup = unroll_lookup(extra_escapable); // 484 // 485 return quoted.replace(extra_escapable, function(a) { // 486 return extra_lookup[a]; // 487 }); // 488 } // 489 // 490 var _all_protocols = ['websocket', // 491 'xdr-streaming', // 492 'xhr-streaming', // 493 'iframe-eventsource', // 494 'iframe-htmlfile', // 495 'xdr-polling', // 496 'xhr-polling', // 497 'iframe-xhr-polling', // 498 'jsonp-polling']; // 499 // 500 utils.probeProtocols = function() { // 501 var probed = {}; // 502 for(var i=0; i<_all_protocols.length; i++) { // 503 var protocol = _all_protocols[i]; // 504 // User can have a typo in protocol name. // 505 probed[protocol] = SockJS[protocol] && // 506 SockJS[protocol].enabled(); // 507 } // 508 return probed; // 509 }; // 510 // 511 utils.detectProtocols = function(probed, protocols_whitelist, info) { // 512 var pe = {}, // 513 protocols = []; // 514 if (!protocols_whitelist) protocols_whitelist = _all_protocols; // 515 for(var i=0; i<protocols_whitelist.length; i++) { // 516 var protocol = protocols_whitelist[i]; // 517 pe[protocol] = probed[protocol]; // 518 } // 519 var maybe_push = function(protos) { // 520 var proto = protos.shift(); // 521 if (pe[proto]) { // 522 protocols.push(proto); // 523 } else { // 524 if (protos.length > 0) { // 525 maybe_push(protos); // 526 } // 527 } // 528 } // 529 // 530 // 1. Websocket // 531 if (info.websocket !== false) { // 532 maybe_push(['websocket']); // 533 } // 534 // 535 // 2. Streaming // 536 if (pe['xhr-streaming'] && !info.null_origin) { // 537 protocols.push('xhr-streaming'); // 538 } else { // 539 if (pe['xdr-streaming'] && !info.cookie_needed && !info.null_origin) { // 540 protocols.push('xdr-streaming'); // 541 } else { // 542 maybe_push(['iframe-eventsource', // 543 'iframe-htmlfile']); // 544 } // 545 } // 546 // 547 // 3. Polling // 548 if (pe['xhr-polling'] && !info.null_origin) { // 549 protocols.push('xhr-polling'); // 550 } else { // 551 if (pe['xdr-polling'] && !info.cookie_needed && !info.null_origin) { // 552 protocols.push('xdr-polling'); // 553 } else { // 554 maybe_push(['iframe-xhr-polling', // 555 'jsonp-polling']); // 556 } // 557 } // 558 return protocols; // 559 } // 560 // [*] End of lib/utils.js // 561 // 562 // 563 // [*] Including lib/dom.js // 564 /* // 565 * ***** BEGIN LICENSE BLOCK ***** // 566 * Copyright (c) 2011-2012 VMware, Inc. // 567 * // 568 * For the license see COPYING. // 569 * ***** END LICENSE BLOCK ***** // 570 */ // 571 // 572 // May be used by htmlfile jsonp and transports. // 573 var MPrefix = '_sockjs_global'; // 574 utils.createHook = function() { // 575 var window_id = 'a' + utils.random_string(8); // 576 if (!(MPrefix in _window)) { // 577 var map = {}; // 578 _window[MPrefix] = function(window_id) { // 579 if (!(window_id in map)) { // 580 map[window_id] = { // 581 id: window_id, // 582 del: function() {delete map[window_id];} // 583 }; // 584 } // 585 return map[window_id]; // 586 } // 587 } // 588 return _window[MPrefix](window_id); // 589 }; // 590 // 591 // 592 // 593 utils.attachMessage = function(listener) { // 594 utils.attachEvent('message', listener); // 595 }; // 596 utils.attachEvent = function(event, listener) { // 597 if (typeof _window.addEventListener !== 'undefined') { // 598 _window.addEventListener(event, listener, false); // 599 } else { // 600 // IE quirks. // 601 // According to: http://stevesouders.com/misc/test-postmessage.php // 602 // the message gets delivered only to 'document', not 'window'. // 603 _document.attachEvent("on" + event, listener); // 604 // I get 'window' for ie8. // 605 _window.attachEvent("on" + event, listener); // 606 } // 607 }; // 608 // 609 utils.detachMessage = function(listener) { // 610 utils.detachEvent('message', listener); // 611 }; // 612 utils.detachEvent = function(event, listener) { // 613 if (typeof _window.addEventListener !== 'undefined') { // 614 _window.removeEventListener(event, listener, false); // 615 } else { // 616 _document.detachEvent("on" + event, listener); // 617 _window.detachEvent("on" + event, listener); // 618 } // 619 }; // 620 // 621 // 622 var on_unload = {}; // 623 // Things registered after beforeunload are to be called immediately. // 624 var after_unload = false; // 625 // 626 var trigger_unload_callbacks = function() { // 627 for(var ref in on_unload) { // 628 on_unload[ref](); // 629 delete on_unload[ref]; // 630 }; // 631 }; // 632 // 633 var unload_triggered = function() { // 634 if(after_unload) return; // 635 after_unload = true; // 636 trigger_unload_callbacks(); // 637 }; // 638 // 639 // 'unload' alone is not reliable in opera within an iframe, but we // 640 // can't use `beforeunload` as IE fires it on javascript: links. // 641 utils.attachEvent('unload', unload_triggered); // 642 // 643 utils.unload_add = function(listener) { // 644 var ref = utils.random_string(8); // 645 on_unload[ref] = listener; // 646 if (after_unload) { // 647 utils.delay(trigger_unload_callbacks); // 648 } // 649 return ref; // 650 }; // 651 utils.unload_del = function(ref) { // 652 if (ref in on_unload) // 653 delete on_unload[ref]; // 654 }; // 655 // 656 // 657 utils.createIframe = function (iframe_url, error_callback) { // 658 var iframe = _document.createElement('iframe'); // 659 var tref, unload_ref; // 660 var unattach = function() { // 661 clearTimeout(tref); // 662 // Explorer had problems with that. // 663 try {iframe.onload = null;} catch (x) {} // 664 iframe.onerror = null; // 665 }; // 666 var cleanup = function() { // 667 if (iframe) { // 668 unattach(); // 669 // This timeout makes chrome fire onbeforeunload event // 670 // within iframe. Without the timeout it goes straight to // 671 // onunload. // 672 setTimeout(function() { // 673 if(iframe) { // 674 iframe.parentNode.removeChild(iframe); // 675 } // 676 iframe = null; // 677 }, 0); // 678 utils.unload_del(unload_ref); // 679 } // 680 }; // 681 var onerror = function(r) { // 682 if (iframe) { // 683 cleanup(); // 684 error_callback(r); // 685 } // 686 }; // 687 var post = function(msg, origin) { // 688 try { // 689 // When the iframe is not loaded, IE raises an exception // 690 // on 'contentWindow'. // 691 if (iframe && iframe.contentWindow) { // 692 iframe.contentWindow.postMessage(msg, origin); // 693 } // 694 } catch (x) {}; // 695 }; // 696 // 697 iframe.src = iframe_url; // 698 iframe.style.display = 'none'; // 699 iframe.style.position = 'absolute'; // 700 iframe.onerror = function(){onerror('onerror');}; // 701 iframe.onload = function() { // 702 // `onload` is triggered before scripts on the iframe are // 703 // executed. Give it few seconds to actually load stuff. // 704 clearTimeout(tref); // 705 tref = setTimeout(function(){onerror('onload timeout');}, 2000); // 706 }; // 707 _document.body.appendChild(iframe); // 708 tref = setTimeout(function(){onerror('timeout');}, 15000); // 709 unload_ref = utils.unload_add(cleanup); // 710 return { // 711 post: post, // 712 cleanup: cleanup, // 713 loaded: unattach // 714 }; // 715 }; // 716 // 717 utils.createHtmlfile = function (iframe_url, error_callback) { // 718 var doc = new ActiveXObject('htmlfile'); // 719 var tref, unload_ref; // 720 var iframe; // 721 var unattach = function() { // 722 clearTimeout(tref); // 723 }; // 724 var cleanup = function() { // 725 if (doc) { // 726 unattach(); // 727 utils.unload_del(unload_ref); // 728 iframe.parentNode.removeChild(iframe); // 729 iframe = doc = null; // 730 CollectGarbage(); // 731 } // 732 }; // 733 var onerror = function(r) { // 734 if (doc) { // 735 cleanup(); // 736 error_callback(r); // 737 } // 738 }; // 739 var post = function(msg, origin) { // 740 try { // 741 // When the iframe is not loaded, IE raises an exception // 742 // on 'contentWindow'. // 743 if (iframe && iframe.contentWindow) { // 744 iframe.contentWindow.postMessage(msg, origin); // 745 } // 746 } catch (x) {}; // 747 }; // 748 // 749 doc.open(); // 750 doc.write('<html><s' + 'cript>' + // 751 'document.domain="' + document.domain + '";' + // 752 '</s' + 'cript></html>'); // 753 doc.close(); // 754 doc.parentWindow[WPrefix] = _window[WPrefix]; // 755 var c = doc.createElement('div'); // 756 doc.body.appendChild(c); // 757 iframe = doc.createElement('iframe'); // 758 c.appendChild(iframe); // 759 iframe.src = iframe_url; // 760 tref = setTimeout(function(){onerror('timeout');}, 15000); // 761 unload_ref = utils.unload_add(cleanup); // 762 return { // 763 post: post, // 764 cleanup: cleanup, // 765 loaded: unattach // 766 }; // 767 }; // 768 // [*] End of lib/dom.js // 769 // 770 // 771 // [*] Including lib/dom2.js // 772 /* // 773 * ***** BEGIN LICENSE BLOCK ***** // 774 * Copyright (c) 2011-2012 VMware, Inc. // 775 * // 776 * For the license see COPYING. // 777 * ***** END LICENSE BLOCK ***** // 778 */ // 779 // 780 var AbstractXHRObject = function(){}; // 781 AbstractXHRObject.prototype = new EventEmitter(['chunk', 'finish']); // 782 // 783 AbstractXHRObject.prototype._start = function(method, url, payload, opts) { // 784 var that = this; // 785 // 786 try { // 787 that.xhr = new XMLHttpRequest(); // 788 } catch(x) {}; // 789 // 790 if (!that.xhr) { // 791 try { // 792 that.xhr = new _window.ActiveXObject('Microsoft.XMLHTTP'); // 793 } catch(x) {}; // 794 } // 795 if (_window.ActiveXObject || _window.XDomainRequest) { // 796 // IE8 caches even POSTs // 797 url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date); // 798 } // 799 // 800 // Explorer tends to keep connection open, even after the // 801 // tab gets closed: http://bugs.jquery.com/ticket/5280 // 802 that.unload_ref = utils.unload_add(function(){that._cleanup(true);}); // 803 try { // 804 that.xhr.open(method, url, true); // 805 } catch(e) { // 806 // IE raises an exception on wrong port. // 807 that.emit('finish', 0, ''); // 808 that._cleanup(); // 809 return; // 810 }; // 811 // 812 if (!opts || !opts.no_credentials) { // 813 // Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest : // 814 // "This never affects same-site requests." // 815 that.xhr.withCredentials = 'true'; // 816 } // 817 if (opts && opts.headers) { // 818 for(var key in opts.headers) { // 819 that.xhr.setRequestHeader(key, opts.headers[key]); // 820 } // 821 } // 822 // 823 that.xhr.onreadystatechange = function() { // 824 if (that.xhr) { // 825 var x = that.xhr; // 826 switch (x.readyState) { // 827 case 3: // 828 // IE doesn't like peeking into responseText or status // 829 // on Microsoft.XMLHTTP and readystate=3 // 830 try { // 831 var status = x.status; // 832 var text = x.responseText; // 833 } catch (x) {}; // 834 // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 // 835 if (status === 1223) status = 204; // 836 // 837 // IE does return readystate == 3 for 404 answers. // 838 if (text && text.length > 0) { // 839 that.emit('chunk', status, text); // 840 } // 841 break; // 842 case 4: // 843 var status = x.status; // 844 // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 // 845 if (status === 1223) status = 204; // 846 // 847 that.emit('finish', status, x.responseText); // 848 that._cleanup(false); // 849 break; // 850 } // 851 } // 852 }; // 853 that.xhr.send(payload); // 854 }; // 855 // 856 AbstractXHRObject.prototype._cleanup = function(abort) { // 857 var that = this; // 858 if (!that.xhr) return; // 859 utils.unload_del(that.unload_ref); // 860 // 861 // IE needs this field to be a function // 862 that.xhr.onreadystatechange = function(){}; // 863 // 864 if (abort) { // 865 try { // 866 that.xhr.abort(); // 867 } catch(x) {}; // 868 } // 869 that.unload_ref = that.xhr = null; // 870 }; // 871 // 872 AbstractXHRObject.prototype.close = function() { // 873 var that = this; // 874 that.nuke(); // 875 that._cleanup(true); // 876 }; // 877 // 878 var XHRCorsObject = utils.XHRCorsObject = function() { // 879 var that = this, args = arguments; // 880 utils.delay(function(){that._start.apply(that, args);}); // 881 }; // 882 XHRCorsObject.prototype = new AbstractXHRObject(); // 883 // 884 var XHRLocalObject = utils.XHRLocalObject = function(method, url, payload) { // 885 var that = this; // 886 utils.delay(function(){ // 887 that._start(method, url, payload, { // 888 no_credentials: true // 889 }); // 890 }); // 891 }; // 892 XHRLocalObject.prototype = new AbstractXHRObject(); // 893 // 894 // 895 // 896 // References: // 897 // http://ajaxian.com/archives/100-line-ajax-wrapper // 898 // http://msdn.microsoft.com/en-us/library/cc288060(v=VS.85).aspx // 899 var XDRObject = utils.XDRObject = function(method, url, payload) { // 900 var that = this; // 901 utils.delay(function(){that._start(method, url, payload);}); // 902 }; // 903 XDRObject.prototype = new EventEmitter(['chunk', 'finish']); // 904 XDRObject.prototype._start = function(method, url, payload) { // 905 var that = this; // 906 var xdr = new XDomainRequest(); // 907 // IE caches even POSTs // 908 url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date); // 909 // 910 var onerror = xdr.ontimeout = xdr.onerror = function() { // 911 that.emit('finish', 0, ''); // 912 that._cleanup(false); // 913 }; // 914 xdr.onprogress = function() { // 915 that.emit('chunk', 200, xdr.responseText); // 916 }; // 917 xdr.onload = function() { // 918 that.emit('finish', 200, xdr.responseText); // 919 that._cleanup(false); // 920 }; // 921 that.xdr = xdr; // 922 that.unload_ref = utils.unload_add(function(){that._cleanup(true);}); // 923 try { // 924 // Fails with AccessDenied if port number is bogus // 925 that.xdr.open(method, url); // 926 that.xdr.send(payload); // 927 } catch(x) { // 928 onerror(); // 929 } // 930 }; // 931 // 932 XDRObject.prototype._cleanup = function(abort) { // 933 var that = this; // 934 if (!that.xdr) return; // 935 utils.unload_del(that.unload_ref); // 936 // 937 that.xdr.ontimeout = that.xdr.onerror = that.xdr.onprogress = // 938 that.xdr.onload = null; // 939 if (abort) { // 940 try { // 941 that.xdr.abort(); // 942 } catch(x) {}; // 943 } // 944 that.unload_ref = that.xdr = null; // 945 }; // 946 // 947 XDRObject.prototype.close = function() { // 948 var that = this; // 949 that.nuke(); // 950 that._cleanup(true); // 951 }; // 952 // 953 // 1. Is natively via XHR // 954 // 2. Is natively via XDR // 955 // 3. Nope, but postMessage is there so it should work via the Iframe. // 956 // 4. Nope, sorry. // 957 utils.isXHRCorsCapable = function() { // 958 if (_window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()) { // 959 return 1; // 960 } // 961 // XDomainRequest doesn't work if page is served from file:// // 962 if (_window.XDomainRequest && _document.domain) { // 963 return 2; // 964 } // 965 if (IframeTransport.enabled()) { // 966 return 3; // 967 } // 968 return 4; // 969 }; // 970 // [*] End of lib/dom2.js // 971 // 972 // 973 // [*] Including lib/sockjs.js // 974 /* // 975 * ***** BEGIN LICENSE BLOCK ***** // 976 * Copyright (c) 2011-2012 VMware, Inc. // 977 * // 978 * For the license see COPYING. // 979 * ***** END LICENSE BLOCK ***** // 980 */ // 981 // 982 var SockJS = function(url, dep_protocols_whitelist, options) { // 983 if (!(this instanceof SockJS)) { // 984 // makes `new` optional // 985 return new SockJS(url, dep_protocols_whitelist, options); // 986 } // 987 // 988 var that = this, protocols_whitelist; // 989 that._options = {devel: false, debug: false, protocols_whitelist: [], // 990 info: undefined, rtt: undefined}; // 991 if (options) { // 992 utils.objectExtend(that._options, options); // 993 } // 994 that._base_url = utils.amendUrl(url); // 995 that._server = that._options.server || utils.random_number_string(1000); // 996 if (that._options.protocols_whitelist && // 997 that._options.protocols_whitelist.length) { // 998 protocols_whitelist = that._options.protocols_whitelist; // 999 } else { // 1000 // Deprecated API // 1001 if (typeof dep_protocols_whitelist === 'string' && // 1002 dep_protocols_whitelist.length > 0) { // 1003 protocols_whitelist = [dep_protocols_whitelist]; // 1004 } else if (utils.isArray(dep_protocols_whitelist)) { // 1005 protocols_whitelist = dep_protocols_whitelist // 1006 } else { // 1007 protocols_whitelist = null; // 1008 } // 1009 if (protocols_whitelist) { // 1010 that._debug('Deprecated API: Use "protocols_whitelist" option ' + // 1011 'instead of supplying protocol list as a second ' + // 1012 'parameter to SockJS constructor.'); // 1013 } // 1014 } // 1015 that._protocols = []; // 1016 that.protocol = null; // 1017 that.readyState = SockJS.CONNECTING; // 1018 that._ir = createInfoReceiver(that._base_url); // 1019 that._ir.onfinish = function(info, rtt) { // 1020 that._ir = null; // 1021 if (info) { // 1022 if (that._options.info) { // 1023 // Override if user supplies the option // 1024 info = utils.objectExtend(info, that._options.info); // 1025 } // 1026 if (that._options.rtt) { // 1027 rtt = that._options.rtt; // 1028 } // 1029 that._applyInfo(info, rtt, protocols_whitelist); // 1030 that._didClose(); // 1031 } else { // 1032 that._didClose(1002, 'Can\'t connect to server', true); // 1033 } // 1034 }; // 1035 }; // 1036 // Inheritance // 1037 SockJS.prototype = new REventTarget(); // 1038 // 1039 SockJS.version = "0.3.4"; // 1040 // 1041 SockJS.CONNECTING = 0; // 1042 SockJS.OPEN = 1; // 1043 SockJS.CLOSING = 2; // 1044 SockJS.CLOSED = 3; // 1045 // 1046 SockJS.prototype._debug = function() { // 1047 if (this._options.debug) // 1048 utils.log.apply(utils, arguments); // 1049 }; // 1050 // 1051 SockJS.prototype._dispatchOpen = function() { // 1052 var that = this; // 1053 if (that.readyState === SockJS.CONNECTING) { // 1054 if (that._transport_tref) { // 1055 clearTimeout(that._transport_tref); // 1056 that._transport_tref = null; // 1057 } // 1058 that.readyState = SockJS.OPEN; // 1059 that.dispatchEvent(new SimpleEvent("open")); // 1060 } else { // 1061 // The server might have been restarted, and lost track of our // 1062 // connection. // 1063 that._didClose(1006, "Server lost session"); // 1064 } // 1065 }; // 1066 // 1067 SockJS.prototype._dispatchMessage = function(data) { // 1068 var that = this; // 1069 if (that.readyState !== SockJS.OPEN) // 1070 return; // 1071 that.dispatchEvent(new SimpleEvent("message", {data: data})); // 1072 }; // 1073 // 1074 SockJS.prototype._dispatchHeartbeat = function(data) { // 1075 var that = this; // 1076 if (that.readyState !== SockJS.OPEN) // 1077 return; // 1078 that.dispatchEvent(new SimpleEvent('heartbeat', {})); // 1079 }; // 1080 // 1081 SockJS.prototype._didClose = function(code, reason, force) { // 1082 var that = this; // 1083 if (that.readyState !== SockJS.CONNECTING && // 1084 that.readyState !== SockJS.OPEN && // 1085 that.readyState !== SockJS.CLOSING) // 1086 throw new Error('INVALID_STATE_ERR'); // 1087 if (that._ir) { // 1088 that._ir.nuke(); // 1089 that._ir = null; // 1090 } // 1091 // 1092 if (that._transport) { // 1093 that._transport.doCleanup(); // 1094 that._transport = null; // 1095 } // 1096 // 1097 var close_event = new SimpleEvent("close", { // 1098 code: code, // 1099 reason: reason, // 1100 wasClean: utils.userSetCode(code)}); // 1101 // 1102 if (!utils.userSetCode(code) && // 1103 that.readyState === SockJS.CONNECTING && !force) { // 1104 if (that._try_next_protocol(close_event)) { // 1105 return; // 1106 } // 1107 close_event = new SimpleEvent("close", {code: 2000, // 1108 reason: "All transports failed", // 1109 wasClean: false, // 1110 last_event: close_event}); // 1111 } // 1112 that.readyState = SockJS.CLOSED; // 1113 // 1114 utils.delay(function() { // 1115 that.dispatchEvent(close_event); // 1116 }); // 1117 }; // 1118 // 1119 SockJS.prototype._didMessage = function(data) { // 1120 var that = this; // 1121 var type = data.slice(0, 1); // 1122 switch(type) { // 1123 case 'o': // 1124 that._dispatchOpen(); // 1125 break; // 1126 case 'a': // 1127 var payload = JSON.parse(data.slice(1) || '[]'); // 1128 for(var i=0; i < payload.length; i++){ // 1129 that._dispatchMessage(payload[i]); // 1130 } // 1131 break; // 1132 case 'm': // 1133 var payload = JSON.parse(data.slice(1) || 'null'); // 1134 that._dispatchMessage(payload); // 1135 break; // 1136 case 'c': // 1137 var payload = JSON.parse(data.slice(1) || '[]'); // 1138 that._didClose(payload[0], payload[1]); // 1139 break; // 1140 case 'h': // 1141 that._dispatchHeartbeat(); // 1142 break; // 1143 } // 1144 }; // 1145 // 1146 SockJS.prototype._try_next_protocol = function(close_event) { // 1147 var that = this; // 1148 if (that.protocol) { // 1149 that._debug('Closed transport:', that.protocol, ''+close_event); // 1150 that.protocol = null; // 1151 } // 1152 if (that._transport_tref) { // 1153 clearTimeout(that._transport_tref); // 1154 that._transport_tref = null; // 1155 } // 1156 // 1157 while(1) { // 1158 var protocol = that.protocol = that._protocols.shift(); // 1159 if (!protocol) { // 1160 return false; // 1161 } // 1162 // Some protocols require access to `body`, what if were in // 1163 // the `head`? // 1164 if (SockJS[protocol] && // 1165 SockJS[protocol].need_body === true && // 1166 (!_document.body || // 1167 (typeof _document.readyState !== 'undefined' // 1168 && _document.readyState !== 'complete'))) { // 1169 that._protocols.unshift(protocol); // 1170 that.protocol = 'waiting-for-load'; // 1171 utils.attachEvent('load', function(){ // 1172 that._try_next_protocol(); // 1173 }); // 1174 return true; // 1175 } // 1176 // 1177 if (!SockJS[protocol] || // 1178 !SockJS[protocol].enabled(that._options)) { // 1179 that._debug('Skipping transport:', protocol); // 1180 } else { // 1181 var roundTrips = SockJS[protocol].roundTrips || 1; // 1182 var to = ((that._options.rto || 0) * roundTrips) || 5000; // 1183 that._transport_tref = utils.delay(to, function() { // 1184 if (that.readyState === SockJS.CONNECTING) { // 1185 // I can't understand how it is possible to run // 1186 // this timer, when the state is CLOSED, but // 1187 // apparently in IE everythin is possible. // 1188 that._didClose(2007, "Transport timeouted"); // 1189 } // 1190 }); // 1191 // 1192 var connid = utils.random_string(8); // 1193 var trans_url = that._base_url + '/' + that._server + '/' + connid; // 1194 that._debug('Opening transport:', protocol, ' url:'+trans_url, // 1195 ' RTO:'+that._options.rto); // 1196 that._transport = new SockJS[protocol](that, trans_url, // 1197 that._base_url); // 1198 return true; // 1199 } // 1200 } // 1201 }; // 1202 // 1203 SockJS.prototype.close = function(code, reason) { // 1204 var that = this; // 1205 if (code && !utils.userSetCode(code)) // 1206 throw new Error("INVALID_ACCESS_ERR"); // 1207 if(that.readyState !== SockJS.CONNECTING && // 1208 that.readyState !== SockJS.OPEN) { // 1209 return false; // 1210 } // 1211 that.readyState = SockJS.CLOSING; // 1212 that._didClose(code || 1000, reason || "Normal closure"); // 1213 return true; // 1214 }; // 1215 // 1216 SockJS.prototype.send = function(data) { // 1217 var that = this; // 1218 if (that.readyState === SockJS.CONNECTING) // 1219 throw new Error('INVALID_STATE_ERR'); // 1220 if (that.readyState === SockJS.OPEN) { // 1221 that._transport.doSend(utils.quote('' + data)); // 1222 } // 1223 return true; // 1224 }; // 1225 // 1226 SockJS.prototype._applyInfo = function(info, rtt, protocols_whitelist) { // 1227 var that = this; // 1228 that._options.info = info; // 1229 that._options.rtt = rtt; // 1230 that._options.rto = utils.countRTO(rtt); // 1231 that._options.info.null_origin = !_document.domain; // 1232 // Servers can override base_url, eg to provide a randomized domain name and // 1233 // avoid browser per-domain connection limits. // 1234 if (info.base_url) // 1235 // <METEOR> // 1236 that._base_url = utils.amendUrl(info.base_url, that._base_url); // 1237 // </METEOR> // 1238 var probed = utils.probeProtocols(); // 1239 that._protocols = utils.detectProtocols(probed, protocols_whitelist, info); // 1240 // <METEOR> // 1241 // https://github.com/sockjs/sockjs-client/issues/79 // 1242 // Hack to avoid XDR when using different protocols // 1243 // We're on IE trying to do cross-protocol. jsonp only. // 1244 if (!utils.isSameOriginScheme(that._base_url) && // 1245 2 === utils.isXHRCorsCapable()) { // 1246 that._protocols = ['jsonp-polling']; // 1247 } // 1248 // </METEOR> // 1249 }; // 1250 // [*] End of lib/sockjs.js // 1251 // 1252 // 1253 // [*] Including lib/trans-websocket.js // 1254 /* // 1255 * ***** BEGIN LICENSE BLOCK ***** // 1256 * Copyright (c) 2011-2012 VMware, Inc. // 1257 * // 1258 * For the license see COPYING. // 1259 * ***** END LICENSE BLOCK ***** // 1260 */ // 1261 // 1262 var WebSocketTransport = SockJS.websocket = function(ri, trans_url) { // 1263 var that = this; // 1264 var url = trans_url + '/websocket'; // 1265 if (url.slice(0, 5) === 'https') { // 1266 url = 'wss' + url.slice(5); // 1267 } else { // 1268 url = 'ws' + url.slice(4); // 1269 } // 1270 that.ri = ri; // 1271 that.url = url; // 1272 var Constructor = _window.WebSocket || _window.MozWebSocket; // 1273 // 1274 that.ws = new Constructor(that.url); // 1275 that.ws.onmessage = function(e) { // 1276 that.ri._didMessage(e.data); // 1277 }; // 1278 // Firefox has an interesting bug. If a websocket connection is // 1279 // created after onunload, it stays alive even when user // 1280 // navigates away from the page. In such situation let's lie - // 1281 // let's not open the ws connection at all. See: // 1282 // https://github.com/sockjs/sockjs-client/issues/28 // 1283 // https://bugzilla.mozilla.org/show_bug.cgi?id=696085 // 1284 that.unload_ref = utils.unload_add(function(){that.ws.close()}); // 1285 that.ws.onclose = function() { // 1286 that.ri._didMessage(utils.closeFrame(1006, "WebSocket connection broken")); // 1287 }; // 1288 }; // 1289 // 1290 WebSocketTransport.prototype.doSend = function(data) { // 1291 this.ws.send('[' + data + ']'); // 1292 }; // 1293 // 1294 WebSocketTransport.prototype.doCleanup = function() { // 1295 var that = this; // 1296 var ws = that.ws; // 1297 if (ws) { // 1298 ws.onmessage = ws.onclose = null; // 1299 ws.close(); // 1300 utils.unload_del(that.unload_ref); // 1301 that.unload_ref = that.ri = that.ws = null; // 1302 } // 1303 }; // 1304 // 1305 WebSocketTransport.enabled = function() { // 1306 return !!(_window.WebSocket || _window.MozWebSocket); // 1307 }; // 1308 // 1309 // In theory, ws should require 1 round trip. But in chrome, this is // 1310 // not very stable over SSL. Most likely a ws connection requires a // 1311 // separate SSL connection, in which case 2 round trips are an // 1312 // absolute minumum. // 1313 WebSocketTransport.roundTrips = 2; // 1314 // [*] End of lib/trans-websocket.js // 1315 // 1316 // 1317 // [*] Including lib/trans-sender.js // 1318 /* // 1319 * ***** BEGIN LICENSE BLOCK ***** // 1320 * Copyright (c) 2011-2012 VMware, Inc. // 1321 * // 1322 * For the license see COPYING. // 1323 * ***** END LICENSE BLOCK ***** // 1324 */ // 1325 // 1326 var BufferedSender = function() {}; // 1327 BufferedSender.prototype.send_constructor = function(sender) { // 1328 var that = this; // 1329 that.send_buffer = []; // 1330 that.sender = sender; // 1331 }; // 1332 BufferedSender.prototype.doSend = function(message) { // 1333 var that = this; // 1334 that.send_buffer.push(message); // 1335 if (!that.send_stop) { // 1336 that.send_schedule(); // 1337 } // 1338 }; // 1339 // 1340 // For polling transports in a situation when in the message callback, // 1341 // new message is being send. If the sending connection was started // 1342 // before receiving one, it is possible to saturate the network and // 1343 // timeout due to the lack of receiving socket. To avoid that we delay // 1344 // sending messages by some small time, in order to let receiving // 1345 // connection be started beforehand. This is only a halfmeasure and // 1346 // does not fix the big problem, but it does make the tests go more // 1347 // stable on slow networks. // 1348 BufferedSender.prototype.send_schedule_wait = function() { // 1349 var that = this; // 1350 var tref; // 1351 that.send_stop = function() { // 1352 that.send_stop = null; // 1353 clearTimeout(tref); // 1354 }; // 1355 tref = utils.delay(25, function() { // 1356 that.send_stop = null; // 1357 that.send_schedule(); // 1358 }); // 1359 }; // 1360 // 1361 BufferedSender.prototype.send_schedule = function() { // 1362 var that = this; // 1363 if (that.send_buffer.length > 0) { // 1364 var payload = '[' + that.send_buffer.join(',') + ']'; // 1365 that.send_stop = that.sender(that.trans_url, payload, function(success, abort_reason) { // 1366 that.send_stop = null; // 1367 if (success === false) { // 1368 that.ri._didClose(1006, 'Sending error ' + abort_reason); // 1369 } else { // 1370 that.send_schedule_wait(); // 1371 } // 1372 }); // 1373 that.send_buffer = []; // 1374 } // 1375 }; // 1376 // 1377 BufferedSender.prototype.send_destructor = function() { // 1378 var that = this; // 1379 if (that._send_stop) { // 1380 that._send_stop(); // 1381 } // 1382 that._send_stop = null; // 1383 }; // 1384 // 1385 var jsonPGenericSender = function(url, payload, callback) { // 1386 var that = this; // 1387 // 1388 if (!('_send_form' in that)) { // 1389 var form = that._send_form = _document.createElement('form'); // 1390 var area = that._send_area = _document.createElement('textarea'); // 1391 area.name = 'd'; // 1392 form.style.display = 'none'; // 1393 form.style.position = 'absolute'; // 1394 form.method = 'POST'; // 1395 form.enctype = 'application/x-www-form-urlencoded'; // 1396 form.acceptCharset = "UTF-8"; // 1397 form.appendChild(area); // 1398 _document.body.appendChild(form); // 1399 } // 1400 var form = that._send_form; // 1401 var area = that._send_area; // 1402 var id = 'a' + utils.random_string(8); // 1403 form.target = id; // 1404 form.action = url + '/jsonp_send?i=' + id; // 1405 // 1406 var iframe; // 1407 try { // 1408 // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) // 1409 iframe = _document.createElement('<iframe name="'+ id +'">'); // 1410 } catch(x) { // 1411 iframe = _document.createElement('iframe'); // 1412 iframe.name = id; // 1413 } // 1414 iframe.id = id; // 1415 form.appendChild(iframe); // 1416 iframe.style.display = 'none'; // 1417 // 1418 try { // 1419 area.value = payload; // 1420 } catch(e) { // 1421 utils.log('Your browser is seriously broken. Go home! ' + e.message); // 1422 } // 1423 form.submit(); // 1424 // 1425 var completed = function(e) { // 1426 if (!iframe.onerror) return; // 1427 iframe.onreadystatechange = iframe.onerror = iframe.onload = null; // 1428 // Opera mini doesn't like if we GC iframe // 1429 // immediately, thus this timeout. // 1430 utils.delay(500, function() { // 1431 iframe.parentNode.removeChild(iframe); // 1432 iframe = null; // 1433 }); // 1434 area.value = ''; // 1435 // It is not possible to detect if the iframe succeeded or // 1436 // failed to submit our form. // 1437 callback(true); // 1438 }; // 1439 iframe.onerror = iframe.onload = completed; // 1440 iframe.onreadystatechange = function(e) { // 1441 if (iframe.readyState == 'complete') completed(); // 1442 }; // 1443 return completed; // 1444 }; // 1445 // 1446 var createAjaxSender = function(AjaxObject) { // 1447 return function(url, payload, callback) { // 1448 var xo = new AjaxObject('POST', url + '/xhr_send', payload); // 1449 xo.onfinish = function(status, text) { // 1450 callback(status === 200 || status === 204, // 1451 'http status ' + status); // 1452 }; // 1453 return function(abort_reason) { // 1454 callback(false, abort_reason); // 1455 }; // 1456 }; // 1457 }; // 1458 // [*] End of lib/trans-sender.js // 1459 // 1460 // 1461 // [*] Including lib/trans-jsonp-receiver.js // 1462 /* // 1463 * ***** BEGIN LICENSE BLOCK ***** // 1464 * Copyright (c) 2011-2012 VMware, Inc. // 1465 * // 1466 * For the license see COPYING. // 1467 * ***** END LICENSE BLOCK ***** // 1468 */ // 1469 // 1470 // Parts derived from Socket.io: // 1471 // https://github.com/LearnBoost/socket.io/blob/0.6.17/lib/socket.io/transports/jsonp-polling.js // 1472 // and jQuery-JSONP: // 1473 // https://code.google.com/p/jquery-jsonp/source/browse/trunk/core/jquery.jsonp.js // 1474 var jsonPGenericReceiver = function(url, callback) { // 1475 var tref; // 1476 var script = _document.createElement('script'); // 1477 var script2; // Opera synchronous load trick. // 1478 var close_script = function(frame) { // 1479 if (script2) { // 1480 script2.parentNode.removeChild(script2); // 1481 script2 = null; // 1482 } // 1483 if (script) { // 1484 clearTimeout(tref); // 1485 // Unfortunately, you can't really abort script loading of // 1486 // the script. // 1487 script.parentNode.removeChild(script); // 1488 script.onreadystatechange = script.onerror = // 1489 script.onload = script.onclick = null; // 1490 script = null; // 1491 callback(frame); // 1492 callback = null; // 1493 } // 1494 }; // 1495 // 1496 // IE9 fires 'error' event after orsc or before, in random order. // 1497 var loaded_okay = false; // 1498 var error_timer = null; // 1499 // 1500 script.id = 'a' + utils.random_string(8); // 1501 script.src = url; // 1502 script.type = 'text/javascript'; // 1503 script.charset = 'UTF-8'; // 1504 script.onerror = function(e) { // 1505 if (!error_timer) { // 1506 // Delay firing close_script. // 1507 error_timer = setTimeout(function() { // 1508 if (!loaded_okay) { // 1509 close_script(utils.closeFrame( // 1510 1006, // 1511 "JSONP script loaded abnormally (onerror)")); // 1512 } // 1513 }, 1000); // 1514 } // 1515 }; // 1516 script.onload = function(e) { // 1517 close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (onload)")); // 1518 }; // 1519 // 1520 script.onreadystatechange = function(e) { // 1521 if (/loaded|closed/.test(script.readyState)) { // 1522 if (script && script.htmlFor && script.onclick) { // 1523 loaded_okay = true; // 1524 try { // 1525 // In IE, actually execute the script. // 1526 script.onclick(); // 1527 } catch (x) {} // 1528 } // 1529 if (script) { // 1530 close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (onreadystatechange)")); // 1531 } // 1532 } // 1533 }; // 1534 // IE: event/htmlFor/onclick trick. // 1535 // One can't rely on proper order for onreadystatechange. In order to // 1536 // make sure, set a 'htmlFor' and 'event' properties, so that // 1537 // script code will be installed as 'onclick' handler for the // 1538 // script object. Later, onreadystatechange, manually execute this // 1539 // code. FF and Chrome doesn't work with 'event' and 'htmlFor' // 1540 // set. For reference see: // 1541 // http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html // 1542 // Also, read on that about script ordering: // 1543 // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order // 1544 if (typeof script.async === 'undefined' && _document.attachEvent) { // 1545 // According to mozilla docs, in recent browsers script.async defaults // 1546 // to 'true', so we may use it to detect a good browser: // 1547 // https://developer.mozilla.org/en/HTML/Element/script // 1548 if (!/opera/i.test(navigator.userAgent)) { // 1549 // Naively assume we're in IE // 1550 try { // 1551 script.htmlFor = script.id; // 1552 script.event = "onclick"; // 1553 } catch (x) {} // 1554 script.async = true; // 1555 } else { // 1556 // Opera, second sync script hack // 1557 script2 = _document.createElement('script'); // 1558 script2.text = "try{var a = document.getElementById('"+script.id+"'); if(a)a.onerror();}catch(x){};"; // 1559 script.async = script2.async = false; // 1560 } // 1561 } // 1562 if (typeof script.async !== 'undefined') { // 1563 script.async = true; // 1564 } // 1565 // 1566 // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty. // 1567 tref = setTimeout(function() { // 1568 close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (timeout)")); // 1569 }, 35000); // 1570 // 1571 var head = _document.getElementsByTagName('head')[0]; // 1572 head.insertBefore(script, head.firstChild); // 1573 if (script2) { // 1574 head.insertBefore(script2, head.firstChild); // 1575 } // 1576 return close_script; // 1577 }; // 1578 // [*] End of lib/trans-jsonp-receiver.js // 1579 // 1580 // 1581 // [*] Including lib/trans-jsonp-polling.js // 1582 /* // 1583 * ***** BEGIN LICENSE BLOCK ***** // 1584 * Copyright (c) 2011-2012 VMware, Inc. // 1585 * // 1586 * For the license see COPYING. // 1587 * ***** END LICENSE BLOCK ***** // 1588 */ // 1589 // 1590 // The simplest and most robust transport, using the well-know cross // 1591 // domain hack - JSONP. This transport is quite inefficient - one // 1592 // mssage could use up to one http request. But at least it works almost // 1593 // everywhere. // 1594 // Known limitations: // 1595 // o you will get a spinning cursor // 1596 // o for Konqueror a dumb timer is needed to detect errors // 1597 // 1598 // 1599 var JsonPTransport = SockJS['jsonp-polling'] = function(ri, trans_url) { // 1600 utils.polluteGlobalNamespace(); // 1601 var that = this; // 1602 that.ri = ri; // 1603 that.trans_url = trans_url; // 1604 that.send_constructor(jsonPGenericSender); // 1605 that._schedule_recv(); // 1606 }; // 1607 // 1608 // Inheritnace // 1609 JsonPTransport.prototype = new BufferedSender(); // 1610 // 1611 JsonPTransport.prototype._schedule_recv = function() { // 1612 var that = this; // 1613 var callback = function(data) { // 1614 that._recv_stop = null; // 1615 if (data) { // 1616 // no data - heartbeat; // 1617 if (!that._is_closing) { // 1618 that.ri._didMessage(data); // 1619 } // 1620 } // 1621 // The message can be a close message, and change is_closing state. // 1622 if (!that._is_closing) { // 1623 that._schedule_recv(); // 1624 } // 1625 }; // 1626 that._recv_stop = jsonPReceiverWrapper(that.trans_url + '/jsonp', // 1627 jsonPGenericReceiver, callback); // 1628 }; // 1629 // 1630 JsonPTransport.enabled = function() { // 1631 return true; // 1632 }; // 1633 // 1634 JsonPTransport.need_body = true; // 1635 // 1636 // 1637 JsonPTransport.prototype.doCleanup = function() { // 1638 var that = this; // 1639 that._is_closing = true; // 1640 if (that._recv_stop) { // 1641 that._recv_stop(); // 1642 } // 1643 that.ri = that._recv_stop = null; // 1644 that.send_destructor(); // 1645 }; // 1646 // 1647 // 1648 // Abstract away code that handles global namespace pollution. // 1649 var jsonPReceiverWrapper = function(url, constructReceiver, user_callback) { // 1650 var id = 'a' + utils.random_string(6); // 1651 var url_id = url + '?c=' + escape(WPrefix + '.' + id); // 1652 // 1653 // Unfortunately it is not possible to abort loading of the // 1654 // script. We need to keep track of frake close frames. // 1655 var aborting = 0; // 1656 // 1657 // Callback will be called exactly once. // 1658 var callback = function(frame) { // 1659 switch(aborting) { // 1660 case 0: // 1661 // Normal behaviour - delete hook _and_ emit message. // 1662 delete _window[WPrefix][id]; // 1663 user_callback(frame); // 1664 break; // 1665 case 1: // 1666 // Fake close frame - emit but don't delete hook. // 1667 user_callback(frame); // 1668 aborting = 2; // 1669 break; // 1670 case 2: // 1671 // Got frame after connection was closed, delete hook, don't emit. // 1672 delete _window[WPrefix][id]; // 1673 break; // 1674 } // 1675 }; // 1676 // 1677 var close_script = constructReceiver(url_id, callback); // 1678 _window[WPrefix][id] = close_script; // 1679 var stop = function() { // 1680 if (_window[WPrefix][id]) { // 1681 aborting = 1; // 1682 _window[WPrefix][id](utils.closeFrame(1000, "JSONP user aborted read")); // 1683 } // 1684 }; // 1685 return stop; // 1686 }; // 1687 // [*] End of lib/trans-jsonp-polling.js // 1688 // 1689 // 1690 // [*] Including lib/trans-xhr.js // 1691 /* // 1692 * ***** BEGIN LICENSE BLOCK ***** // 1693 * Copyright (c) 2011-2012 VMware, Inc. // 1694 * // 1695 * For the license see COPYING. // 1696 * ***** END LICENSE BLOCK ***** // 1697 */ // 1698 // 1699 var AjaxBasedTransport = function() {}; // 1700 AjaxBasedTransport.prototype = new BufferedSender(); // 1701 // 1702 AjaxBasedTransport.prototype.run = function(ri, trans_url, // 1703 url_suffix, Receiver, AjaxObject) { // 1704 var that = this; // 1705 that.ri = ri; // 1706 that.trans_url = trans_url; // 1707 that.send_constructor(createAjaxSender(AjaxObject)); // 1708 that.poll = new Polling(ri, Receiver, // 1709 trans_url + url_suffix, AjaxObject); // 1710 }; // 1711 // 1712 AjaxBasedTransport.prototype.doCleanup = function() { // 1713 var that = this; // 1714 if (that.poll) { // 1715 that.poll.abort(); // 1716 that.poll = null; // 1717 } // 1718 }; // 1719 // 1720 // xhr-streaming // 1721 var XhrStreamingTransport = SockJS['xhr-streaming'] = function(ri, trans_url) { // 1722 this.run(ri, trans_url, '/xhr_streaming', XhrReceiver, utils.XHRCorsObject); // 1723 }; // 1724 // 1725 XhrStreamingTransport.prototype = new AjaxBasedTransport(); // 1726 // 1727 XhrStreamingTransport.enabled = function() { // 1728 // Support for CORS Ajax aka Ajax2? Opera 12 claims CORS but // 1729 // doesn't do streaming. // 1730 return (_window.XMLHttpRequest && // 1731 'withCredentials' in new XMLHttpRequest() && // 1732 (!/opera/i.test(navigator.userAgent))); // 1733 }; // 1734 XhrStreamingTransport.roundTrips = 2; // preflight, ajax // 1735 // 1736 // Safari gets confused when a streaming ajax request is started // 1737 // before onload. This causes the load indicator to spin indefinetely. // 1738 XhrStreamingTransport.need_body = true; // 1739 // 1740 // 1741 // According to: // 1742 // http://stackoverflow.com/questions/1641507/detect-browser-support-for-cross-domain-xmlhttprequests // 1743 // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ // 1744 // 1745 // 1746 // xdr-streaming // 1747 var XdrStreamingTransport = SockJS['xdr-streaming'] = function(ri, trans_url) { // 1748 this.run(ri, trans_url, '/xhr_streaming', XhrReceiver, utils.XDRObject); // 1749 }; // 1750 // 1751 XdrStreamingTransport.prototype = new AjaxBasedTransport(); // 1752 // 1753 XdrStreamingTransport.enabled = function() { // 1754 return !!_window.XDomainRequest; // 1755 }; // 1756 XdrStreamingTransport.roundTrips = 2; // preflight, ajax // 1757 // 1758 // 1759 // 1760 // xhr-polling // 1761 var XhrPollingTransport = SockJS['xhr-polling'] = function(ri, trans_url) { // 1762 this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XHRCorsObject); // 1763 }; // 1764 // 1765 XhrPollingTransport.prototype = new AjaxBasedTransport(); // 1766 // 1767 XhrPollingTransport.enabled = XhrStreamingTransport.enabled; // 1768 XhrPollingTransport.roundTrips = 2; // preflight, ajax // 1769 // 1770 // 1771 // xdr-polling // 1772 var XdrPollingTransport = SockJS['xdr-polling'] = function(ri, trans_url) { // 1773 this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XDRObject); // 1774 }; // 1775 // 1776 XdrPollingTransport.prototype = new AjaxBasedTransport(); // 1777 // 1778 XdrPollingTransport.enabled = XdrStreamingTransport.enabled; // 1779 XdrPollingTransport.roundTrips = 2; // preflight, ajax // 1780 // [*] End of lib/trans-xhr.js // 1781 // 1782 // 1783 // [*] Including lib/trans-iframe.js // 1784 /* // 1785 * ***** BEGIN LICENSE BLOCK ***** // 1786 * Copyright (c) 2011-2012 VMware, Inc. // 1787 * // 1788 * For the license see COPYING. // 1789 * ***** END LICENSE BLOCK ***** // 1790 */ // 1791 // 1792 // Few cool transports do work only for same-origin. In order to make // 1793 // them working cross-domain we shall use iframe, served form the // 1794 // remote domain. New browsers, have capabilities to communicate with // 1795 // cross domain iframe, using postMessage(). In IE it was implemented // 1796 // from IE 8+, but of course, IE got some details wrong: // 1797 // http://msdn.microsoft.com/en-us/library/cc197015(v=VS.85).aspx // 1798 // http://stevesouders.com/misc/test-postmessage.php // 1799 // 1800 var IframeTransport = function() {}; // 1801 // 1802 IframeTransport.prototype.i_constructor = function(ri, trans_url, base_url) { // 1803 var that = this; // 1804 that.ri = ri; // 1805 that.origin = utils.getOrigin(base_url); // 1806 that.base_url = base_url; // 1807 that.trans_url = trans_url; // 1808 // 1809 var iframe_url = base_url + '/iframe.html'; // 1810 if (that.ri._options.devel) { // 1811 iframe_url += '?t=' + (+new Date); // 1812 } // 1813 that.window_id = utils.random_string(8); // 1814 iframe_url += '#' + that.window_id; // 1815 // 1816 that.iframeObj = utils.createIframe(iframe_url, function(r) { // 1817 that.ri._didClose(1006, "Unable to load an iframe (" + r + ")"); // 1818 }); // 1819 // 1820 that.onmessage_cb = utils.bind(that.onmessage, that); // 1821 utils.attachMessage(that.onmessage_cb); // 1822 }; // 1823 // 1824 IframeTransport.prototype.doCleanup = function() { // 1825 var that = this; // 1826 if (that.iframeObj) { // 1827 utils.detachMessage(that.onmessage_cb); // 1828 try { // 1829 // When the iframe is not loaded, IE raises an exception // 1830 // on 'contentWindow'. // 1831 if (that.iframeObj.iframe.contentWindow) { // 1832 that.postMessage('c'); // 1833 } // 1834 } catch (x) {} // 1835 that.iframeObj.cleanup(); // 1836 that.iframeObj = null; // 1837 that.onmessage_cb = that.iframeObj = null; // 1838 } // 1839 }; // 1840 // 1841 IframeTransport.prototype.onmessage = function(e) { // 1842 var that = this; // 1843 if (e.origin !== that.origin) return; // 1844 var window_id = e.data.slice(0, 8); // 1845 var type = e.data.slice(8, 9); // 1846 var data = e.data.slice(9); // 1847 // 1848 if (window_id !== that.window_id) return; // 1849 // 1850 switch(type) { // 1851 case 's': // 1852 that.iframeObj.loaded(); // 1853 that.postMessage('s', JSON.stringify([SockJS.version, that.protocol, that.trans_url, that.base_url])); // 1854 break; // 1855 case 't': // 1856 that.ri._didMessage(data); // 1857 break; // 1858 } // 1859 }; // 1860 // 1861 IframeTransport.prototype.postMessage = function(type, data) { // 1862 var that = this; // 1863 that.iframeObj.post(that.window_id + type + (data || ''), that.origin); // 1864 }; // 1865 // 1866 IframeTransport.prototype.doSend = function (message) { // 1867 this.postMessage('m', message); // 1868 }; // 1869 // 1870 IframeTransport.enabled = function() { // 1871 // postMessage misbehaves in konqueror 4.6.5 - the messages are delivered with // 1872 // huge delay, or not at all. // 1873 var konqueror = navigator && navigator.userAgent && navigator.userAgent.indexOf('Konqueror') !== -1; // 1874 return ((typeof _window.postMessage === 'function' || // 1875 typeof _window.postMessage === 'object') && (!konqueror)); // 1876 }; // 1877 // [*] End of lib/trans-iframe.js // 1878 // 1879 // 1880 // [*] Including lib/trans-iframe-within.js // 1881 /* // 1882 * ***** BEGIN LICENSE BLOCK ***** // 1883 * Copyright (c) 2011-2012 VMware, Inc. // 1884 * // 1885 * For the license see COPYING. // 1886 * ***** END LICENSE BLOCK ***** // 1887 */ // 1888 // 1889 var curr_window_id; // 1890 // 1891 var postMessage = function (type, data) { // 1892 if(parent !== _window) { // 1893 parent.postMessage(curr_window_id + type + (data || ''), '*'); // 1894 } else { // 1895 utils.log("Can't postMessage, no parent window.", type, data); // 1896 } // 1897 }; // 1898 // 1899 var FacadeJS = function() {}; // 1900 FacadeJS.prototype._didClose = function (code, reason) { // 1901 postMessage('t', utils.closeFrame(code, reason)); // 1902 }; // 1903 FacadeJS.prototype._didMessage = function (frame) { // 1904 postMessage('t', frame); // 1905 }; // 1906 FacadeJS.prototype._doSend = function (data) { // 1907 this._transport.doSend(data); // 1908 }; // 1909 FacadeJS.prototype._doCleanup = function () { // 1910 this._transport.doCleanup(); // 1911 }; // 1912 // 1913 utils.parent_origin = undefined; // 1914 // 1915 SockJS.bootstrap_iframe = function() { // 1916 var facade; // 1917 curr_window_id = _document.location.hash.slice(1); // 1918 var onMessage = function(e) { // 1919 if(e.source !== parent) return; // 1920 if(typeof utils.parent_origin === 'undefined') // 1921 utils.parent_origin = e.origin; // 1922 if (e.origin !== utils.parent_origin) return; // 1923 // 1924 var window_id = e.data.slice(0, 8); // 1925 var type = e.data.slice(8, 9); // 1926 var data = e.data.slice(9); // 1927 if (window_id !== curr_window_id) return; // 1928 switch(type) { // 1929 case 's': // 1930 var p = JSON.parse(data); // 1931 var version = p[0]; // 1932 var protocol = p[1]; // 1933 var trans_url = p[2]; // 1934 var base_url = p[3]; // 1935 if (version !== SockJS.version) { // 1936 utils.log("Incompatibile SockJS! Main site uses:" + // 1937 " \"" + version + "\", the iframe:" + // 1938 " \"" + SockJS.version + "\"."); // 1939 } // 1940 if (!utils.flatUrl(trans_url) || !utils.flatUrl(base_url)) { // 1941 utils.log("Only basic urls are supported in SockJS"); // 1942 return; // 1943 } // 1944 // 1945 if (!utils.isSameOriginUrl(trans_url) || // 1946 !utils.isSameOriginUrl(base_url)) { // 1947 utils.log("Can't connect to different domain from within an " + // 1948 "iframe. (" + JSON.stringify([_window.location.href, trans_url, base_url]) + // 1949 ")"); // 1950 return; // 1951 } // 1952 facade = new FacadeJS(); // 1953 facade._transport = new FacadeJS[protocol](facade, trans_url, base_url); // 1954 break; // 1955 case 'm': // 1956 facade._doSend(data); // 1957 break; // 1958 case 'c': // 1959 if (facade) // 1960 facade._doCleanup(); // 1961 facade = null; // 1962 break; // 1963 } // 1964 }; // 1965 // 1966 // alert('test ticker'); // 1967 // facade = new FacadeJS(); // 1968 // facade._transport = new FacadeJS['w-iframe-xhr-polling'](facade, 'http://host.com:9999/ticker/12/basd'); // 1969 // 1970 utils.attachMessage(onMessage); // 1971 // 1972 // Start // 1973 postMessage('s'); // 1974 }; // 1975 // [*] End of lib/trans-iframe-within.js // 1976 // 1977 // 1978 // [*] Including lib/info.js // 1979 /* // 1980 * ***** BEGIN LICENSE BLOCK ***** // 1981 * Copyright (c) 2011-2012 VMware, Inc. // 1982 * // 1983 * For the license see COPYING. // 1984 * ***** END LICENSE BLOCK ***** // 1985 */ // 1986 // 1987 var InfoReceiver = function(base_url, AjaxObject) { // 1988 var that = this; // 1989 utils.delay(function(){that.doXhr(base_url, AjaxObject);}); // 1990 }; // 1991 // 1992 InfoReceiver.prototype = new EventEmitter(['finish']); // 1993 // 1994 InfoReceiver.prototype.doXhr = function(base_url, AjaxObject) { // 1995 var that = this; // 1996 var t0 = (new Date()).getTime(); // 1997 // 1998 // <METEOR> // 1999 // https://github.com/sockjs/sockjs-client/pull/129 // 2000 // var xo = new AjaxObject('GET', base_url + '/info'); // 2001 // 2002 var xo = new AjaxObject( // 2003 // add cachebusting parameter to url to work around a chrome bug: // 2004 // https://code.google.com/p/chromium/issues/detail?id=263981 // 2005 // or misbehaving proxies. // 2006 'GET', base_url + '/info?cb=' + utils.random_string(10)) // 2007 // </METEOR> // 2008 // 2009 var tref = utils.delay(8000, // 2010 function(){xo.ontimeout();}); // 2011 // 2012 xo.onfinish = function(status, text) { // 2013 clearTimeout(tref); // 2014 tref = null; // 2015 if (status === 200) { // 2016 var rtt = (new Date()).getTime() - t0; // 2017 var info = JSON.parse(text); // 2018 if (typeof info !== 'object') info = {}; // 2019 that.emit('finish', info, rtt); // 2020 } else { // 2021 that.emit('finish'); // 2022 } // 2023 }; // 2024 xo.ontimeout = function() { // 2025 xo.close(); // 2026 that.emit('finish'); // 2027 }; // 2028 }; // 2029 // 2030 var InfoReceiverIframe = function(base_url) { // 2031 var that = this; // 2032 var go = function() { // 2033 var ifr = new IframeTransport(); // 2034 ifr.protocol = 'w-iframe-info-receiver'; // 2035 var fun = function(r) { // 2036 if (typeof r === 'string' && r.substr(0,1) === 'm') { // 2037 var d = JSON.parse(r.substr(1)); // 2038 var info = d[0], rtt = d[1]; // 2039 that.emit('finish', info, rtt); // 2040 } else { // 2041 that.emit('finish'); // 2042 } // 2043 ifr.doCleanup(); // 2044 ifr = null; // 2045 }; // 2046 var mock_ri = { // 2047 _options: {}, // 2048 _didClose: fun, // 2049 _didMessage: fun // 2050 }; // 2051 ifr.i_constructor(mock_ri, base_url, base_url); // 2052 } // 2053 if(!_document.body) { // 2054 utils.attachEvent('load', go); // 2055 } else { // 2056 go(); // 2057 } // 2058 }; // 2059 InfoReceiverIframe.prototype = new EventEmitter(['finish']); // 2060 // 2061 // 2062 var InfoReceiverFake = function() { // 2063 // It may not be possible to do cross domain AJAX to get the info // 2064 // data, for example for IE7. But we want to run JSONP, so let's // 2065 // fake the response, with rtt=2s (rto=6s). // 2066 var that = this; // 2067 utils.delay(function() { // 2068 that.emit('finish', {}, 2000); // 2069 }); // 2070 }; // 2071 InfoReceiverFake.prototype = new EventEmitter(['finish']); // 2072 // 2073 var createInfoReceiver = function(base_url) { // 2074 if (utils.isSameOriginUrl(base_url)) { // 2075 // If, for some reason, we have SockJS locally - there's no // 2076 // need to start up the complex machinery. Just use ajax. // 2077 return new InfoReceiver(base_url, utils.XHRLocalObject); // 2078 } // 2079 switch (utils.isXHRCorsCapable()) { // 2080 case 1: // 2081 // XHRLocalObject -> no_credentials=true // 2082 return new InfoReceiver(base_url, utils.XHRLocalObject); // 2083 case 2: // 2084 // <METEOR> // 2085 // https://github.com/sockjs/sockjs-client/issues/79 // 2086 // XDR doesn't work across different schemes // 2087 // http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx if (utils.isSameOriginScheme(base_url)) // 2089 return new InfoReceiver(base_url, utils.XDRObject); // 2090 else // 2091 return new InfoReceiverFake(); // 2092 // </METEOR> // 2093 case 3: // 2094 // Opera // 2095 return new InfoReceiverIframe(base_url); // 2096 default: // 2097 // IE 7 // 2098 return new InfoReceiverFake(); // 2099 }; // 2100 }; // 2101 // 2102 // 2103 var WInfoReceiverIframe = FacadeJS['w-iframe-info-receiver'] = function(ri, _trans_url, base_url) { // 2104 var ir = new InfoReceiver(base_url, utils.XHRLocalObject); // 2105 ir.onfinish = function(info, rtt) { // 2106 ri._didMessage('m'+JSON.stringify([info, rtt])); // 2107 ri._didClose(); // 2108 } // 2109 }; // 2110 WInfoReceiverIframe.prototype.doCleanup = function() {}; // 2111 // [*] End of lib/info.js // 2112 // 2113 // 2114 // [*] Including lib/trans-iframe-eventsource.js // 2115 /* // 2116 * ***** BEGIN LICENSE BLOCK ***** // 2117 * Copyright (c) 2011-2012 VMware, Inc. // 2118 * // 2119 * For the license see COPYING. // 2120 * ***** END LICENSE BLOCK ***** // 2121 */ // 2122 // 2123 var EventSourceIframeTransport = SockJS['iframe-eventsource'] = function () { // 2124 var that = this; // 2125 that.protocol = 'w-iframe-eventsource'; // 2126 that.i_constructor.apply(that, arguments); // 2127 }; // 2128 // 2129 EventSourceIframeTransport.prototype = new IframeTransport(); // 2130 // 2131 EventSourceIframeTransport.enabled = function () { // 2132 return ('EventSource' in _window) && IframeTransport.enabled(); // 2133 }; // 2134 // 2135 EventSourceIframeTransport.need_body = true; // 2136 EventSourceIframeTransport.roundTrips = 3; // html, javascript, eventsource // 2137 // 2138 // 2139 // w-iframe-eventsource // 2140 var EventSourceTransport = FacadeJS['w-iframe-eventsource'] = function(ri, trans_url) { // 2141 this.run(ri, trans_url, '/eventsource', EventSourceReceiver, utils.XHRLocalObject); // 2142 } // 2143 EventSourceTransport.prototype = new AjaxBasedTransport(); // 2144 // [*] End of lib/trans-iframe-eventsource.js // 2145 // 2146 // 2147 // [*] Including lib/trans-iframe-xhr-polling.js // 2148 /* // 2149 * ***** BEGIN LICENSE BLOCK ***** // 2150 * Copyright (c) 2011-2012 VMware, Inc. // 2151 * // 2152 * For the license see COPYING. // 2153 * ***** END LICENSE BLOCK ***** // 2154 */ // 2155 // 2156 var XhrPollingIframeTransport = SockJS['iframe-xhr-polling'] = function () { // 2157 var that = this; // 2158 that.protocol = 'w-iframe-xhr-polling'; // 2159 that.i_constructor.apply(that, arguments); // 2160 }; // 2161 // 2162 XhrPollingIframeTransport.prototype = new IframeTransport(); // 2163 // 2164 XhrPollingIframeTransport.enabled = function () { // 2165 return _window.XMLHttpRequest && IframeTransport.enabled(); // 2166 }; // 2167 // 2168 XhrPollingIframeTransport.need_body = true; // 2169 XhrPollingIframeTransport.roundTrips = 3; // html, javascript, xhr // 2170 // 2171 // 2172 // w-iframe-xhr-polling // 2173 var XhrPollingITransport = FacadeJS['w-iframe-xhr-polling'] = function(ri, trans_url) { // 2174 this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XHRLocalObject); // 2175 }; // 2176 // 2177 XhrPollingITransport.prototype = new AjaxBasedTransport(); // 2178 // [*] End of lib/trans-iframe-xhr-polling.js // 2179 // 2180 // 2181 // [*] Including lib/trans-iframe-htmlfile.js // 2182 /* // 2183 * ***** BEGIN LICENSE BLOCK ***** // 2184 * Copyright (c) 2011-2012 VMware, Inc. // 2185 * // 2186 * For the license see COPYING. // 2187 * ***** END LICENSE BLOCK ***** // 2188 */ // 2189 // 2190 // This transport generally works in any browser, but will cause a // 2191 // spinning cursor to appear in any browser other than IE. // 2192 // We may test this transport in all browsers - why not, but in // 2193 // production it should be only run in IE. // 2194 // 2195 var HtmlFileIframeTransport = SockJS['iframe-htmlfile'] = function () { // 2196 var that = this; // 2197 that.protocol = 'w-iframe-htmlfile'; // 2198 that.i_constructor.apply(that, arguments); // 2199 }; // 2200 // 2201 // Inheritance. // 2202 HtmlFileIframeTransport.prototype = new IframeTransport(); // 2203 // 2204 HtmlFileIframeTransport.enabled = function() { // 2205 return IframeTransport.enabled(); // 2206 }; // 2207 // 2208 HtmlFileIframeTransport.need_body = true; // 2209 HtmlFileIframeTransport.roundTrips = 3; // html, javascript, htmlfile // 2210 // 2211 // 2212 // w-iframe-htmlfile // 2213 var HtmlFileTransport = FacadeJS['w-iframe-htmlfile'] = function(ri, trans_url) { // 2214 this.run(ri, trans_url, '/htmlfile', HtmlfileReceiver, utils.XHRLocalObject); // 2215 }; // 2216 HtmlFileTransport.prototype = new AjaxBasedTransport(); // 2217 // [*] End of lib/trans-iframe-htmlfile.js // 2218 // 2219 // 2220 // [*] Including lib/trans-polling.js // 2221 /* // 2222 * ***** BEGIN LICENSE BLOCK ***** // 2223 * Copyright (c) 2011-2012 VMware, Inc. // 2224 * // 2225 * For the license see COPYING. // 2226 * ***** END LICENSE BLOCK ***** // 2227 */ // 2228 // 2229 var Polling = function(ri, Receiver, recv_url, AjaxObject) { // 2230 var that = this; // 2231 that.ri = ri; // 2232 that.Receiver = Receiver; // 2233 that.recv_url = recv_url; // 2234 that.AjaxObject = AjaxObject; // 2235 that._scheduleRecv(); // 2236 }; // 2237 // 2238 Polling.prototype._scheduleRecv = function() { // 2239 var that = this; // 2240 var poll = that.poll = new that.Receiver(that.recv_url, that.AjaxObject); // 2241 var msg_counter = 0; // 2242 poll.onmessage = function(e) { // 2243 msg_counter += 1; // 2244 that.ri._didMessage(e.data); // 2245 }; // 2246 poll.onclose = function(e) { // 2247 that.poll = poll = poll.onmessage = poll.onclose = null; // 2248 if (!that.poll_is_closing) { // 2249 if (e.reason === 'permanent') { // 2250 that.ri._didClose(1006, 'Polling error (' + e.reason + ')'); // 2251 } else { // 2252 that._scheduleRecv(); // 2253 } // 2254 } // 2255 }; // 2256 }; // 2257 // 2258 Polling.prototype.abort = function() { // 2259 var that = this; // 2260 that.poll_is_closing = true; // 2261 if (that.poll) { // 2262 that.poll.abort(); // 2263 } // 2264 }; // 2265 // [*] End of lib/trans-polling.js // 2266 // 2267 // 2268 // [*] Including lib/trans-receiver-eventsource.js // 2269 /* // 2270 * ***** BEGIN LICENSE BLOCK ***** // 2271 * Copyright (c) 2011-2012 VMware, Inc. // 2272 * // 2273 * For the license see COPYING. // 2274 * ***** END LICENSE BLOCK ***** // 2275 */ // 2276 // 2277 var EventSourceReceiver = function(url) { // 2278 var that = this; // 2279 var es = new EventSource(url); // 2280 es.onmessage = function(e) { // 2281 that.dispatchEvent(new SimpleEvent('message', // 2282 {'data': unescape(e.data)})); // 2283 }; // 2284 that.es_close = es.onerror = function(e, abort_reason) { // 2285 // ES on reconnection has readyState = 0 or 1. // 2286 // on network error it's CLOSED = 2 // 2287 var reason = abort_reason ? 'user' : // 2288 (es.readyState !== 2 ? 'network' : 'permanent'); // 2289 that.es_close = es.onmessage = es.onerror = null; // 2290 // EventSource reconnects automatically. // 2291 es.close(); // 2292 es = null; // 2293 // Safari and chrome < 15 crash if we close window before // 2294 // waiting for ES cleanup. See: // 2295 // https://code.google.com/p/chromium/issues/detail?id=89155 // 2296 utils.delay(200, function() { // 2297 that.dispatchEvent(new SimpleEvent('close', {reason: reason})); // 2298 }); // 2299 }; // 2300 }; // 2301 // 2302 EventSourceReceiver.prototype = new REventTarget(); // 2303 // 2304 EventSourceReceiver.prototype.abort = function() { // 2305 var that = this; // 2306 if (that.es_close) { // 2307 that.es_close({}, true); // 2308 } // 2309 }; // 2310 // [*] End of lib/trans-receiver-eventsource.js // 2311 // 2312 // 2313 // [*] Including lib/trans-receiver-htmlfile.js // 2314 /* // 2315 * ***** BEGIN LICENSE BLOCK ***** // 2316 * Copyright (c) 2011-2012 VMware, Inc. // 2317 * // 2318 * For the license see COPYING. // 2319 * ***** END LICENSE BLOCK ***** // 2320 */ // 2321 // 2322 var _is_ie_htmlfile_capable; // 2323 var isIeHtmlfileCapable = function() { // 2324 if (_is_ie_htmlfile_capable === undefined) { // 2325 if ('ActiveXObject' in _window) { // 2326 try { // 2327 _is_ie_htmlfile_capable = !!new ActiveXObject('htmlfile'); // 2328 } catch (x) {} // 2329 } else { // 2330 _is_ie_htmlfile_capable = false; // 2331 } // 2332 } // 2333 return _is_ie_htmlfile_capable; // 2334 }; // 2335 // 2336 // 2337 var HtmlfileReceiver = function(url) { // 2338 var that = this; // 2339 utils.polluteGlobalNamespace(); // 2340 // 2341 that.id = 'a' + utils.random_string(6, 26); // 2342 url += ((url.indexOf('?') === -1) ? '?' : '&') + // 2343 'c=' + escape(WPrefix + '.' + that.id); // 2344 // 2345 var constructor = isIeHtmlfileCapable() ? // 2346 utils.createHtmlfile : utils.createIframe; // 2347 // 2348 var iframeObj; // 2349 _window[WPrefix][that.id] = { // 2350 start: function () { // 2351 iframeObj.loaded(); // 2352 }, // 2353 message: function (data) { // 2354 that.dispatchEvent(new SimpleEvent('message', {'data': data})); // 2355 }, // 2356 stop: function () { // 2357 that.iframe_close({}, 'network'); // 2358 } // 2359 }; // 2360 that.iframe_close = function(e, abort_reason) { // 2361 iframeObj.cleanup(); // 2362 that.iframe_close = iframeObj = null; // 2363 delete _window[WPrefix][that.id]; // 2364 that.dispatchEvent(new SimpleEvent('close', {reason: abort_reason})); // 2365 }; // 2366 iframeObj = constructor(url, function(e) { // 2367 that.iframe_close({}, 'permanent'); // 2368 }); // 2369 }; // 2370 // 2371 HtmlfileReceiver.prototype = new REventTarget(); // 2372 // 2373 HtmlfileReceiver.prototype.abort = function() { // 2374 var that = this; // 2375 if (that.iframe_close) { // 2376 that.iframe_close({}, 'user'); // 2377 } // 2378 }; // 2379 // [*] End of lib/trans-receiver-htmlfile.js // 2380 // 2381 // 2382 // [*] Including lib/trans-receiver-xhr.js // 2383 /* // 2384 * ***** BEGIN LICENSE BLOCK ***** // 2385 * Copyright (c) 2011-2012 VMware, Inc. // 2386 * // 2387 * For the license see COPYING. // 2388 * ***** END LICENSE BLOCK ***** // 2389 */ // 2390 // 2391 var XhrReceiver = function(url, AjaxObject) { // 2392 var that = this; // 2393 var buf_pos = 0; // 2394 // 2395 that.xo = new AjaxObject('POST', url, null); // 2396 that.xo.onchunk = function(status, text) { // 2397 if (status !== 200) return; // 2398 while (1) { // 2399 var buf = text.slice(buf_pos); // 2400 var p = buf.indexOf('\n'); // 2401 if (p === -1) break; // 2402 buf_pos += p+1; // 2403 var msg = buf.slice(0, p); // 2404 that.dispatchEvent(new SimpleEvent('message', {data: msg})); // 2405 } // 2406 }; // 2407 that.xo.onfinish = function(status, text) { // 2408 that.xo.onchunk(status, text); // 2409 that.xo = null; // 2410 var reason = status === 200 ? 'network' : 'permanent'; // 2411 that.dispatchEvent(new SimpleEvent('close', {reason: reason})); // 2412 } // 2413 }; // 2414 // 2415 XhrReceiver.prototype = new REventTarget(); // 2416 // 2417 XhrReceiver.prototype.abort = function() { // 2418 var that = this; // 2419 if (that.xo) { // 2420 that.xo.close(); // 2421 that.dispatchEvent(new SimpleEvent('close', {reason: 'user'})); // 2422 that.xo = null; // 2423 } // 2424 }; // 2425 // [*] End of lib/trans-receiver-xhr.js // 2426 // 2427 // 2428 // [*] Including lib/test-hooks.js // 2429 /* // 2430 * ***** BEGIN LICENSE BLOCK ***** // 2431 * Copyright (c) 2011-2012 VMware, Inc. // 2432 * // 2433 * For the license see COPYING. // 2434 * ***** END LICENSE BLOCK ***** // 2435 */ // 2436 // 2437 // For testing // 2438 SockJS.getUtils = function(){ // 2439 return utils; // 2440 }; // 2441 // 2442 SockJS.getIframeTransport = function(){ // 2443 return IframeTransport; // 2444 }; // 2445 // [*] End of lib/test-hooks.js // 2446 // 2447 return SockJS; // 2448 })(); // 2449 if ('_sockjs_onload' in window) setTimeout(_sockjs_onload, 1); // 2450 // 2451 // AMD compliance // 2452 if (typeof define === 'function' && define.amd) { // 2453 define('sockjs', [], function(){return SockJS;}); // 2454 } // 2455 // [*] End of lib/index.js // 2456 // 2457 // [*] End of lib/all.js // 2458 // 2459 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/stream_client_sockjs.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // @param url {String} URL to Meteor app // 1 // "http://subdomain.meteor.com/" or "/" or // 2 // "ddp+sockjs://foo-**.meteor.com/sockjs" // 3 LivedataTest.ClientStream = function (url, options) { // 4 var self = this; // 5 self.options = _.extend({ // 6 retry: true // 7 }, options); // 8 self._initCommon(self.options); // 9 // 10 //// Constants // 11 // 12 // 13 // how long between hearing heartbeat from the server until we declare // 14 // the connection dead. heartbeats come every 45s (stream_server.js) // 15 // // 16 // NOTE: this is a older timeout mechanism. We now send heartbeats at // 17 // the DDP level (https://github.com/meteor/meteor/pull/1865), and // 18 // expect those timeouts to kill a non-responsive connection before // 19 // this timeout fires. This is kept around for compatibility (when // 20 // talking to a server that doesn't support DDP heartbeats) and can be // 21 // removed later. // 22 self.HEARTBEAT_TIMEOUT = 100*1000; // 23 // 24 self.rawUrl = url; // 25 self.socket = null; // 26 // 27 self.heartbeatTimer = null; // 28 // 29 // Listen to global 'online' event if we are running in a browser. // 30 // (IE8 does not support addEventListener) // 31 if (typeof window !== 'undefined' && window.addEventListener) // 32 window.addEventListener("online", _.bind(self._online, self), // 33 false /* useCapture. make FF3.6 happy. */); // 34 // 35 //// Kickoff! // 36 self._launchConnection(); // 37 }; // 38 // 39 _.extend(LivedataTest.ClientStream.prototype, { // 40 // 41 // data is a utf8 string. Data sent while not connected is dropped on // 42 // the floor, and it is up the user of this API to retransmit lost // 43 // messages on 'reset' // 44 send: function (data) { // 45 var self = this; // 46 if (self.currentStatus.connected) { // 47 self.socket.send(data); // 48 } // 49 }, // 50 // 51 // Changes where this connection points // 52 _changeUrl: function (url) { // 53 var self = this; // 54 self.rawUrl = url; // 55 }, // 56 // 57 _connected: function () { // 58 var self = this; // 59 // 60 if (self.connectionTimer) { // 61 clearTimeout(self.connectionTimer); // 62 self.connectionTimer = null; // 63 } // 64 // 65 if (self.currentStatus.connected) { // 66 // already connected. do nothing. this probably shouldn't happen. // 67 return; // 68 } // 69 // 70 // update status // 71 self.currentStatus.status = "connected"; // 72 self.currentStatus.connected = true; // 73 self.currentStatus.retryCount = 0; // 74 self.statusChanged(); // 75 // 76 // fire resets. This must come after status change so that clients // 77 // can call send from within a reset callback. // 78 _.each(self.eventCallbacks.reset, function (callback) { callback(); }); // 79 // 80 }, // 81 // 82 _cleanup: function (maybeError) { // 83 var self = this; // 84 // 85 self._clearConnectionAndHeartbeatTimers(); // 86 if (self.socket) { // 87 self.socket.onmessage = self.socket.onclose // 88 = self.socket.onerror = self.socket.onheartbeat = function () {}; // 89 self.socket.close(); // 90 self.socket = null; // 91 } // 92 // 93 _.each(self.eventCallbacks.disconnect, function (callback) { // 94 callback(maybeError); // 95 }); // 96 }, // 97 // 98 _clearConnectionAndHeartbeatTimers: function () { // 99 var self = this; // 100 if (self.connectionTimer) { // 101 clearTimeout(self.connectionTimer); // 102 self.connectionTimer = null; // 103 } // 104 if (self.heartbeatTimer) { // 105 clearTimeout(self.heartbeatTimer); // 106 self.heartbeatTimer = null; // 107 } // 108 }, // 109 // 110 _heartbeat_timeout: function () { // 111 var self = this; // 112 Meteor._debug("Connection timeout. No sockjs heartbeat received."); // 113 self._lostConnection(new DDP.ConnectionError("Heartbeat timed out")); // 114 }, // 115 // 116 _heartbeat_received: function () { // 117 var self = this; // 118 // If we've already permanently shut down this stream, the timeout is // 119 // already cleared, and we don't need to set it again. // 120 if (self._forcedToDisconnect) // 121 return; // 122 if (self.heartbeatTimer) // 123 clearTimeout(self.heartbeatTimer); // 124 self.heartbeatTimer = setTimeout( // 125 _.bind(self._heartbeat_timeout, self), // 126 self.HEARTBEAT_TIMEOUT); // 127 }, // 128 // 129 _sockjsProtocolsWhitelist: function () { // 130 // only allow polling protocols. no streaming. streaming // 131 // makes safari spin. // 132 var protocolsWhitelist = [ // 133 'xdr-polling', 'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling']; // 134 // 135 // iOS 4 and 5 and below crash when using websockets over certain // 136 // proxies. this seems to be resolved with iOS 6. eg // 137 // https://github.com/LearnBoost/socket.io/issues/193#issuecomment-7308865. // 138 // // 139 // iOS <4 doesn't support websockets at all so sockjs will just // 140 // immediately fall back to http // 141 var noWebsockets = navigator && // 142 /iPhone|iPad|iPod/.test(navigator.userAgent) && // 143 /OS 4_|OS 5_/.test(navigator.userAgent); // 144 // 145 if (!noWebsockets) // 146 protocolsWhitelist = ['websocket'].concat(protocolsWhitelist); // 147 // 148 return protocolsWhitelist; // 149 }, // 150 // 151 _launchConnection: function () { // 152 var self = this; // 153 self._cleanup(); // cleanup the old socket, if there was one. // 154 // 155 var options = _.extend({ // 156 protocols_whitelist:self._sockjsProtocolsWhitelist() // 157 }, self.options._sockjsOptions); // 158 // 159 // Convert raw URL to SockJS URL each time we open a connection, so that we // 160 // can connect to random hostnames and get around browser per-host // 161 // connection limits. // 162 self.socket = new SockJS(toSockjsUrl(self.rawUrl), undefined, options); // 163 self.socket.onopen = function (data) { // 164 self._connected(); // 165 }; // 166 self.socket.onmessage = function (data) { // 167 self._heartbeat_received(); // 168 // 169 if (self.currentStatus.connected) // 170 _.each(self.eventCallbacks.message, function (callback) { // 171 callback(data.data); // 172 }); // 173 }; // 174 self.socket.onclose = function () { // 175 self._lostConnection(); // 176 }; // 177 self.socket.onerror = function () { // 178 // XXX is this ever called? // 179 Meteor._debug("stream error", _.toArray(arguments), (new Date()).toDateString()); // 180 }; // 181 // 182 self.socket.onheartbeat = function () { // 183 self._heartbeat_received(); // 184 }; // 185 // 186 if (self.connectionTimer) // 187 clearTimeout(self.connectionTimer); // 188 self.connectionTimer = setTimeout(function () { // 189 self._lostConnection( // 190 new DDP.ConnectionError("DDP connection timed out")); // 191 }, self.CONNECT_TIMEOUT); // 192 } // 193 }); // 194 // 195 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/stream_client_common.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // XXX from Underscore.String (http://epeli.github.com/underscore.string/) // 1 var startsWith = function(str, starts) { // 2 return str.length >= starts.length && // 3 str.substring(0, starts.length) === starts; // 4 }; // 5 var endsWith = function(str, ends) { // 6 return str.length >= ends.length && // 7 str.substring(str.length - ends.length) === ends; // 8 }; // 9 // 10 // @param url {String} URL to Meteor app, eg: // 11 // "/" or "madewith.meteor.com" or "https://foo.meteor.com" // 12 // or "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" // 13 // @returns {String} URL to the endpoint with the specific scheme and subPath, e.g. // 14 // for scheme "http" and subPath "sockjs" // 15 // "http://subdomain.meteor.com/sockjs" or "/sockjs" // 16 // or "https://ddp--1234-foo.meteor.com/sockjs" // 17 var translateUrl = function(url, newSchemeBase, subPath) { // 18 if (! newSchemeBase) { // 19 newSchemeBase = "http"; // 20 } // 21 // 22 var ddpUrlMatch = url.match(/^ddp(i?)\+sockjs:\/\//); // 23 var httpUrlMatch = url.match(/^http(s?):\/\//); // 24 var newScheme; // 25 if (ddpUrlMatch) { // 26 // Remove scheme and split off the host. // 27 var urlAfterDDP = url.substr(ddpUrlMatch[0].length); // 28 newScheme = ddpUrlMatch[1] === "i" ? newSchemeBase : newSchemeBase + "s"; // 29 var slashPos = urlAfterDDP.indexOf('/'); // 30 var host = // 31 slashPos === -1 ? urlAfterDDP : urlAfterDDP.substr(0, slashPos); // 32 var rest = slashPos === -1 ? '' : urlAfterDDP.substr(slashPos); // 33 // 34 // In the host (ONLY!), change '*' characters into random digits. This // 35 // allows different stream connections to connect to different hostnames // 36 // and avoid browser per-hostname connection limits. // 37 host = host.replace(/\*/g, function () { // 38 return Math.floor(Random.fraction()*10); // 39 }); // 40 // 41 return newScheme + '://' + host + rest; // 42 } else if (httpUrlMatch) { // 43 newScheme = !httpUrlMatch[1] ? newSchemeBase : newSchemeBase + "s"; // 44 var urlAfterHttp = url.substr(httpUrlMatch[0].length); // 45 url = newScheme + "://" + urlAfterHttp; // 46 } // 47 // 48 // Prefix FQDNs but not relative URLs // 49 if (url.indexOf("://") === -1 && !startsWith(url, "/")) { // 50 url = newSchemeBase + "://" + url; // 51 } // 52 // 53 // XXX This is not what we should be doing: if I have a site // 54 // deployed at "/foo", then DDP.connect("/") should actually connect // 55 // to "/", not to "/foo". "/" is an absolute path. (Contrast: if // 56 // deployed at "/foo", it would be reasonable for DDP.connect("bar") // 57 // to connect to "/foo/bar"). // 58 // // 59 // We should make this properly honor absolute paths rather than // 60 // forcing the path to be relative to the site root. Simultaneously, // 61 // we should set DDP_DEFAULT_CONNECTION_URL to include the site // 62 // root. See also client_convenience.js #RationalizingRelativeDDPURLs // 63 url = Meteor._relativeToSiteRootUrl(url); // 64 // 65 if (endsWith(url, "/")) // 66 return url + subPath; // 67 else // 68 return url + "/" + subPath; // 69 }; // 70 // 71 toSockjsUrl = function (url) { // 72 return translateUrl(url, "http", "sockjs"); // 73 }; // 74 // 75 toWebsocketUrl = function (url) { // 76 var ret = translateUrl(url, "ws", "websocket"); // 77 return ret; // 78 }; // 79 // 80 LivedataTest.toSockjsUrl = toSockjsUrl; // 81 // 82 // 83 _.extend(LivedataTest.ClientStream.prototype, { // 84 // 85 // Register for callbacks. // 86 on: function (name, callback) { // 87 var self = this; // 88 // 89 if (name !== 'message' && name !== 'reset' && name !== 'disconnect') // 90 throw new Error("unknown event type: " + name); // 91 // 92 if (!self.eventCallbacks[name]) // 93 self.eventCallbacks[name] = []; // 94 self.eventCallbacks[name].push(callback); // 95 }, // 96 // 97 // 98 _initCommon: function (options) { // 99 var self = this; // 100 options = options || {}; // 101 // 102 //// Constants // 103 // 104 // how long to wait until we declare the connection attempt // 105 // failed. // 106 self.CONNECT_TIMEOUT = options.connectTimeoutMs || 10000; // 107 // 108 self.eventCallbacks = {}; // name -> [callback] // 109 // 110 self._forcedToDisconnect = false; // 111 // 112 //// Reactive status // 113 self.currentStatus = { // 114 status: "connecting", // 115 connected: false, // 116 retryCount: 0 // 117 }; // 118 // 119 // 120 self.statusListeners = typeof Tracker !== 'undefined' && new Tracker.Dependency; // 121 self.statusChanged = function () { // 122 if (self.statusListeners) // 123 self.statusListeners.changed(); // 124 }; // 125 // 126 //// Retry logic // 127 self._retry = new Retry; // 128 self.connectionTimer = null; // 129 // 130 }, // 131 // 132 // Trigger a reconnect. // 133 reconnect: function (options) { // 134 var self = this; // 135 options = options || {}; // 136 // 137 if (options.url) { // 138 self._changeUrl(options.url); // 139 } // 140 // 141 if (options._sockjsOptions) { // 142 self.options._sockjsOptions = options._sockjsOptions; // 143 } // 144 // 145 if (self.currentStatus.connected) { // 146 if (options._force || options.url) { // 147 // force reconnect. // 148 self._lostConnection(new DDP.ForcedReconnectError); // 149 } // else, noop. // 150 return; // 151 } // 152 // 153 // if we're mid-connection, stop it. // 154 if (self.currentStatus.status === "connecting") { // 155 // Pretend it's a clean close. // 156 self._lostConnection(); // 157 } // 158 // 159 self._retry.clear(); // 160 self.currentStatus.retryCount -= 1; // don't count manual retries // 161 self._retryNow(); // 162 }, // 163 // 164 disconnect: function (options) { // 165 var self = this; // 166 options = options || {}; // 167 // 168 // Failed is permanent. If we're failed, don't let people go back // 169 // online by calling 'disconnect' then 'reconnect'. // 170 if (self._forcedToDisconnect) // 171 return; // 172 // 173 // If _permanent is set, permanently disconnect a stream. Once a stream // 174 // is forced to disconnect, it can never reconnect. This is for // 175 // error cases such as ddp version mismatch, where trying again // 176 // won't fix the problem. // 177 if (options._permanent) { // 178 self._forcedToDisconnect = true; // 179 } // 180 // 181 self._cleanup(); // 182 self._retry.clear(); // 183 // 184 self.currentStatus = { // 185 status: (options._permanent ? "failed" : "offline"), // 186 connected: false, // 187 retryCount: 0 // 188 }; // 189 // 190 if (options._permanent && options._error) // 191 self.currentStatus.reason = options._error; // 192 // 193 self.statusChanged(); // 194 }, // 195 // 196 // maybeError is set unless it's a clean protocol-level close. // 197 _lostConnection: function (maybeError) { // 198 var self = this; // 199 // 200 self._cleanup(maybeError); // 201 self._retryLater(maybeError); // sets status. no need to do it here. // 202 }, // 203 // 204 // fired when we detect that we've gone online. try to reconnect // 205 // immediately. // 206 _online: function () { // 207 // if we've requested to be offline by disconnecting, don't reconnect. // 208 if (this.currentStatus.status != "offline") // 209 this.reconnect(); // 210 }, // 211 // 212 _retryLater: function (maybeError) { // 213 var self = this; // 214 // 215 var timeout = 0; // 216 if (self.options.retry || // 217 (maybeError && maybeError.errorType === "DDP.ForcedReconnectError")) { // 218 timeout = self._retry.retryLater( // 219 self.currentStatus.retryCount, // 220 _.bind(self._retryNow, self) // 221 ); // 222 self.currentStatus.status = "waiting"; // 223 self.currentStatus.retryTime = (new Date()).getTime() + timeout; // 224 } else { // 225 self.currentStatus.status = "failed"; // 226 delete self.currentStatus.retryTime; // 227 } // 228 // 229 self.currentStatus.connected = false; // 230 self.statusChanged(); // 231 }, // 232 // 233 _retryNow: function () { // 234 var self = this; // 235 // 236 if (self._forcedToDisconnect) // 237 return; // 238 // 239 self.currentStatus.retryCount += 1; // 240 self.currentStatus.status = "connecting"; // 241 self.currentStatus.connected = false; // 242 delete self.currentStatus.retryTime; // 243 self.statusChanged(); // 244 // 245 self._launchConnection(); // 246 }, // 247 // 248 // 249 // Get current status. Reactive. // 250 status: function () { // 251 var self = this; // 252 if (self.statusListeners) // 253 self.statusListeners.depend(); // 254 return self.currentStatus; // 255 } // 256 }); // 257 // 258 DDP.ConnectionError = Meteor.makeErrorType( // 259 "DDP.ConnectionError", function (message) { // 260 var self = this; // 261 self.message = message; // 262 }); // 263 // 264 DDP.ForcedReconnectError = Meteor.makeErrorType( // 265 "DDP.ForcedReconnectError", function () {}); // 266 // 267 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/heartbeat.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Heartbeat options: // 1 // heartbeatInterval: interval to send pings, in milliseconds. // 2 // heartbeatTimeout: timeout to close the connection if a reply isn't // 3 // received, in milliseconds. // 4 // sendPing: function to call to send a ping on the connection. // 5 // onTimeout: function to call to close the connection. // 6 // 7 Heartbeat = function (options) { // 8 var self = this; // 9 // 10 self.heartbeatInterval = options.heartbeatInterval; // 11 self.heartbeatTimeout = options.heartbeatTimeout; // 12 self._sendPing = options.sendPing; // 13 self._onTimeout = options.onTimeout; // 14 // 15 self._heartbeatIntervalHandle = null; // 16 self._heartbeatTimeoutHandle = null; // 17 }; // 18 // 19 _.extend(Heartbeat.prototype, { // 20 stop: function () { // 21 var self = this; // 22 self._clearHeartbeatIntervalTimer(); // 23 self._clearHeartbeatTimeoutTimer(); // 24 }, // 25 // 26 start: function () { // 27 var self = this; // 28 self.stop(); // 29 self._startHeartbeatIntervalTimer(); // 30 }, // 31 // 32 _startHeartbeatIntervalTimer: function () { // 33 var self = this; // 34 self._heartbeatIntervalHandle = Meteor.setTimeout( // 35 _.bind(self._heartbeatIntervalFired, self), // 36 self.heartbeatInterval // 37 ); // 38 }, // 39 // 40 _startHeartbeatTimeoutTimer: function () { // 41 var self = this; // 42 self._heartbeatTimeoutHandle = Meteor.setTimeout( // 43 _.bind(self._heartbeatTimeoutFired, self), // 44 self.heartbeatTimeout // 45 ); // 46 }, // 47 // 48 _clearHeartbeatIntervalTimer: function () { // 49 var self = this; // 50 if (self._heartbeatIntervalHandle) { // 51 Meteor.clearTimeout(self._heartbeatIntervalHandle); // 52 self._heartbeatIntervalHandle = null; // 53 } // 54 }, // 55 // 56 _clearHeartbeatTimeoutTimer: function () { // 57 var self = this; // 58 if (self._heartbeatTimeoutHandle) { // 59 Meteor.clearTimeout(self._heartbeatTimeoutHandle); // 60 self._heartbeatTimeoutHandle = null; // 61 } // 62 }, // 63 // 64 // The heartbeat interval timer is fired when we should send a ping. // 65 _heartbeatIntervalFired: function () { // 66 var self = this; // 67 self._heartbeatIntervalHandle = null; // 68 self._sendPing(); // 69 // Wait for a pong. // 70 self._startHeartbeatTimeoutTimer(); // 71 }, // 72 // 73 // The heartbeat timeout timer is fired when we sent a ping, but we // 74 // timed out waiting for the pong. // 75 _heartbeatTimeoutFired: function () { // 76 var self = this; // 77 self._heartbeatTimeoutHandle = null; // 78 self._onTimeout(); // 79 }, // 80 // 81 pingReceived: function () { // 82 var self = this; // 83 // We know the connection is alive if we receive a ping, so we // 84 // don't need to send a ping ourselves. Reset the interval timer. // 85 if (self._heartbeatIntervalHandle) { // 86 self._clearHeartbeatIntervalTimer(); // 87 self._startHeartbeatIntervalTimer(); // 88 } // 89 }, // 90 // 91 pongReceived: function () { // 92 var self = this; // 93 // 94 // Receiving a pong means we won't timeout, so clear the timeout // 95 // timer and start the interval again. // 96 if (self._heartbeatTimeoutHandle) { // 97 self._clearHeartbeatTimeoutTimer(); // 98 self._startHeartbeatIntervalTimer(); // 99 } // 100 } // 101 }); // 102 // 103 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/livedata_common.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // All the supported versions (for both the client and server) // 1 // These must be in order of preference; most favored-first // 2 SUPPORTED_DDP_VERSIONS = [ '1', 'pre2', 'pre1' ]; // 3 // 4 LivedataTest.SUPPORTED_DDP_VERSIONS = SUPPORTED_DDP_VERSIONS; // 5 // 6 // Instance name is this because it is usually referred to as this inside a // 7 // method definition // 8 /** // 9 * @summary The state for a single invocation of a method, referenced by this // 10 * inside a method definition. // 11 * @param {Object} options // 12 * @instanceName this // 13 */ // 14 MethodInvocation = function (options) { // 15 var self = this; // 16 // 17 // true if we're running not the actual method, but a stub (that is, // 18 // if we're on a client (which may be a browser, or in the future a // 19 // server connecting to another server) and presently running a // 20 // simulation of a server-side method for latency compensation // 21 // purposes). not currently true except in a client such as a browser, // 22 // since there's usually no point in running stubs unless you have a // 23 // zero-latency connection to the user. // 24 // 25 /** // 26 * @summary Access inside a method invocation. Boolean value, true if this invocation is a stub. // 27 * @locus Anywhere // 28 * @name isSimulation // 29 * @memberOf MethodInvocation // 30 * @instance // 31 * @type {Boolean} // 32 */ // 33 this.isSimulation = options.isSimulation; // 34 // 35 // call this function to allow other method invocations (from the // 36 // same client) to continue running without waiting for this one to // 37 // complete. // 38 this._unblock = options.unblock || function () {}; // 39 this._calledUnblock = false; // 40 // 41 // current user id // 42 // 43 /** // 44 * @summary The id of the user that made this method call, or `null` if no user was logged in. // 45 * @locus Anywhere // 46 * @name userId // 47 * @memberOf MethodInvocation // 48 * @instance // 49 */ // 50 this.userId = options.userId; // 51 // 52 // sets current user id in all appropriate server contexts and // 53 // reruns subscriptions // 54 this._setUserId = options.setUserId || function () {}; // 55 // 56 // On the server, the connection this method call came in on. // 57 // 58 /** // 59 * @summary Access inside a method invocation. The [connection](#meteor_onconnection) that this method was received on. `null` if the method is not associated with a connection, eg. a server initiated method call. * @locus Server // 61 * @name connection // 62 * @memberOf MethodInvocation // 63 * @instance // 64 */ // 65 this.connection = options.connection; // 66 // 67 // The seed for randomStream value generation // 68 this.randomSeed = options.randomSeed; // 69 // 70 // This is set by RandomStream.get; and holds the random stream state // 71 this.randomStream = null; // 72 }; // 73 // 74 _.extend(MethodInvocation.prototype, { // 75 /** // 76 * @summary Call inside a method invocation. Allow subsequent method from this client to begin running in a new fiber. * @locus Server // 78 * @memberOf MethodInvocation // 79 * @instance // 80 */ // 81 unblock: function () { // 82 var self = this; // 83 self._calledUnblock = true; // 84 self._unblock(); // 85 }, // 86 // 87 /** // 88 * @summary Set the logged in user. // 89 * @locus Server // 90 * @memberOf MethodInvocation // 91 * @instance // 92 * @param {String | null} userId The value that should be returned by `userId` on this connection. // 93 */ // 94 setUserId: function(userId) { // 95 var self = this; // 96 if (self._calledUnblock) // 97 throw new Error("Can't call setUserId in a method after calling unblock"); // 98 self.userId = userId; // 99 self._setUserId(userId); // 100 } // 101 }); // 102 // 103 parseDDP = function (stringMessage) { // 104 try { // 105 var msg = JSON.parse(stringMessage); // 106 } catch (e) { // 107 Meteor._debug("Discarding message with invalid JSON", stringMessage); // 108 return null; // 109 } // 110 // DDP messages must be objects. // 111 if (msg === null || typeof msg !== 'object') { // 112 Meteor._debug("Discarding non-object DDP message", stringMessage); // 113 return null; // 114 } // 115 // 116 // massage msg to get it into "abstract ddp" rather than "wire ddp" format. // 117 // 118 // switch between "cleared" rep of unsetting fields and "undefined" // 119 // rep of same // 120 if (_.has(msg, 'cleared')) { // 121 if (!_.has(msg, 'fields')) // 122 msg.fields = {}; // 123 _.each(msg.cleared, function (clearKey) { // 124 msg.fields[clearKey] = undefined; // 125 }); // 126 delete msg.cleared; // 127 } // 128 // 129 _.each(['fields', 'params', 'result'], function (field) { // 130 if (_.has(msg, field)) // 131 msg[field] = EJSON._adjustTypesFromJSONValue(msg[field]); // 132 }); // 133 // 134 return msg; // 135 }; // 136 // 137 stringifyDDP = function (msg) { // 138 var copy = EJSON.clone(msg); // 139 // swizzle 'changed' messages from 'fields undefined' rep to 'fields // 140 // and cleared' rep // 141 if (_.has(msg, 'fields')) { // 142 var cleared = []; // 143 _.each(msg.fields, function (value, key) { // 144 if (value === undefined) { // 145 cleared.push(key); // 146 delete copy.fields[key]; // 147 } // 148 }); // 149 if (!_.isEmpty(cleared)) // 150 copy.cleared = cleared; // 151 if (_.isEmpty(copy.fields)) // 152 delete copy.fields; // 153 } // 154 // adjust types to basic // 155 _.each(['fields', 'params', 'result'], function (field) { // 156 if (_.has(copy, field)) // 157 copy[field] = EJSON._adjustTypesToJSONValue(copy[field]); // 158 }); // 159 if (msg.id && typeof msg.id !== 'string') { // 160 throw new Error("Message id is not a string"); // 161 } // 162 return JSON.stringify(copy); // 163 }; // 164 // 165 // This is private but it's used in a few places. accounts-base uses // 166 // it to get the current user. accounts-password uses it to stash SRP // 167 // state in the DDP session. Meteor.setTimeout and friends clear // 168 // it. We can probably find a better way to factor this. // 169 DDP._CurrentInvocation = new Meteor.EnvironmentVariable; // 170 // 171 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/random_stream.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // RandomStream allows for generation of pseudo-random values, from a seed. // 1 // // 2 // We use this for consistent 'random' numbers across the client and server. // 3 // We want to generate probably-unique IDs on the client, and we ideally want // 4 // the server to generate the same IDs when it executes the method. // 5 // // 6 // For generated values to be the same, we must seed ourselves the same way, // 7 // and we must keep track of the current state of our pseudo-random generators. // 8 // We call this state the scope. By default, we use the current DDP method // 9 // invocation as our scope. DDP now allows the client to specify a randomSeed. // 10 // If a randomSeed is provided it will be used to seed our random sequences. // 11 // In this way, client and server method calls will generate the same values. // 12 // // 13 // We expose multiple named streams; each stream is independent // 14 // and is seeded differently (but predictably from the name). // 15 // By using multiple streams, we support reordering of requests, // 16 // as long as they occur on different streams. // 17 // // 18 // @param options {Optional Object} // 19 // seed: Array or value - Seed value(s) for the generator. // 20 // If an array, will be used as-is // 21 // If a value, will be converted to a single-value array // 22 // If omitted, a random array will be used as the seed. // 23 RandomStream = function (options) { // 24 var self = this; // 25 // 26 this.seed = [].concat(options.seed || randomToken()); // 27 // 28 this.sequences = {}; // 29 }; // 30 // 31 // Returns a random string of sufficient length for a random seed. // 32 // This is a placeholder function; a similar function is planned // 33 // for Random itself; when that is added we should remove this function, // 34 // and call Random's randomToken instead. // 35 function randomToken() { // 36 return Random.hexString(20); // 37 }; // 38 // 39 // Returns the random stream with the specified name, in the specified scope. // 40 // If scope is null (or otherwise falsey) then we will use Random, which will // 41 // give us as random numbers as possible, but won't produce the same // 42 // values across client and server. // 43 // However, scope will normally be the current DDP method invocation, so // 44 // we'll use the stream with the specified name, and we should get consistent // 45 // values on the client and server sides of a method call. // 46 RandomStream.get = function (scope, name) { // 47 if (!name) { // 48 name = "default"; // 49 } // 50 if (!scope) { // 51 // There was no scope passed in; // 52 // the sequence won't actually be reproducible. // 53 return Random; // 54 } // 55 var randomStream = scope.randomStream; // 56 if (!randomStream) { // 57 scope.randomStream = randomStream = new RandomStream({ // 58 seed: scope.randomSeed // 59 }); // 60 } // 61 return randomStream._sequence(name); // 62 }; // 63 // 64 // Returns the named sequence of pseudo-random values. // 65 // The scope will be DDP._CurrentInvocation.get(), so the stream will produce // 66 // consistent values for method calls on the client and server. // 67 DDP.randomStream = function (name) { // 68 var scope = DDP._CurrentInvocation.get(); // 69 return RandomStream.get(scope, name); // 70 }; // 71 // 72 // Creates a randomSeed for passing to a method call. // 73 // Note that we take enclosing as an argument, // 74 // though we expect it to be DDP._CurrentInvocation.get() // 75 // However, we often evaluate makeRpcSeed lazily, and thus the relevant // 76 // invocation may not be the one currently in scope. // 77 // If enclosing is null, we'll use Random and values won't be repeatable. // 78 makeRpcSeed = function (enclosing, methodName) { // 79 var stream = RandomStream.get(enclosing, '/rpc/' + methodName); // 80 return stream.hexString(20); // 81 }; // 82 // 83 _.extend(RandomStream.prototype, { // 84 // Get a random sequence with the specified name, creating it if does not exist. // 85 // New sequences are seeded with the seed concatenated with the name. // 86 // By passing a seed into Random.create, we use the Alea generator. // 87 _sequence: function (name) { // 88 var self = this; // 89 // 90 var sequence = self.sequences[name] || null; // 91 if (sequence === null) { // 92 var sequenceSeed = self.seed.concat(name); // 93 for (var i = 0; i < sequenceSeed.length; i++) { // 94 if (_.isFunction(sequenceSeed[i])) { // 95 sequenceSeed[i] = sequenceSeed[i](); // 96 } // 97 } // 98 self.sequences[name] = sequence = Random.createWithSeeds.apply(null, sequenceSeed); // 99 } // 100 return sequence; // 101 } // 102 }); // 103 // 104 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/livedata_connection.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if (Meteor.isServer) { // 1 var path = Npm.require('path'); // 2 var Fiber = Npm.require('fibers'); // 3 var Future = Npm.require(path.join('fibers', 'future')); // 4 } // 5 // 6 // @param url {String|Object} URL to Meteor app, // 7 // or an object as a test hook (see code) // 8 // Options: // 9 // reloadWithOutstanding: is it OK to reload if there are outstanding methods? // 10 // headers: extra headers to send on the websockets connection, for // 11 // server-to-server DDP only // 12 // _sockjsOptions: Specifies options to pass through to the sockjs client // 13 // onDDPNegotiationVersionFailure: callback when version negotiation fails. // 14 // // 15 // XXX There should be a way to destroy a DDP connection, causing all // 16 // outstanding method calls to fail. // 17 // // 18 // XXX Our current way of handling failure and reconnection is great // 19 // for an app (where we want to tolerate being disconnected as an // 20 // expect state, and keep trying forever to reconnect) but cumbersome // 21 // for something like a command line tool that wants to make a // 22 // connection, call a method, and print an error if connection // 23 // fails. We should have better usability in the latter case (while // 24 // still transparently reconnecting if it's just a transient failure // 25 // or the server migrating us). // 26 var Connection = function (url, options) { // 27 var self = this; // 28 options = _.extend({ // 29 onConnected: function () {}, // 30 onDDPVersionNegotiationFailure: function (description) { // 31 Meteor._debug(description); // 32 }, // 33 heartbeatInterval: 35000, // 34 heartbeatTimeout: 15000, // 35 // These options are only for testing. // 36 reloadWithOutstanding: false, // 37 supportedDDPVersions: SUPPORTED_DDP_VERSIONS, // 38 retry: true, // 39 respondToPings: true // 40 }, options); // 41 // 42 // If set, called when we reconnect, queuing method calls _before_ the // 43 // existing outstanding ones. This is the only data member that is part of the // 44 // public API! // 45 self.onReconnect = null; // 46 // 47 // as a test hook, allow passing a stream instead of a url. // 48 if (typeof url === "object") { // 49 self._stream = url; // 50 } else { // 51 self._stream = new LivedataTest.ClientStream(url, { // 52 retry: options.retry, // 53 headers: options.headers, // 54 _sockjsOptions: options._sockjsOptions, // 55 // Used to keep some tests quiet, or for other cases in which // 56 // the right thing to do with connection errors is to silently // 57 // fail (e.g. sending package usage stats). At some point we // 58 // should have a real API for handling client-stream-level // 59 // errors. // 60 _dontPrintErrors: options._dontPrintErrors, // 61 connectTimeoutMs: options.connectTimeoutMs // 62 }); // 63 } // 64 // 65 self._lastSessionId = null; // 66 self._versionSuggestion = null; // The last proposed DDP version. // 67 self._version = null; // The DDP version agreed on by client and server. // 68 self._stores = {}; // name -> object with methods // 69 self._methodHandlers = {}; // name -> func // 70 self._nextMethodId = 1; // 71 self._supportedDDPVersions = options.supportedDDPVersions; // 72 // 73 self._heartbeatInterval = options.heartbeatInterval; // 74 self._heartbeatTimeout = options.heartbeatTimeout; // 75 // 76 // Tracks methods which the user has tried to call but which have not yet // 77 // called their user callback (ie, they are waiting on their result or for all // 78 // of their writes to be written to the local cache). Map from method ID to // 79 // MethodInvoker object. // 80 self._methodInvokers = {}; // 81 // 82 // Tracks methods which the user has called but whose result messages have not // 83 // arrived yet. // 84 // // 85 // _outstandingMethodBlocks is an array of blocks of methods. Each block // 86 // represents a set of methods that can run at the same time. The first block // 87 // represents the methods which are currently in flight; subsequent blocks // 88 // must wait for previous blocks to be fully finished before they can be sent // 89 // to the server. // 90 // // 91 // Each block is an object with the following fields: // 92 // - methods: a list of MethodInvoker objects // 93 // - wait: a boolean; if true, this block had a single method invoked with // 94 // the "wait" option // 95 // // 96 // There will never be adjacent blocks with wait=false, because the only thing // 97 // that makes methods need to be serialized is a wait method. // 98 // // 99 // Methods are removed from the first block when their "result" is // 100 // received. The entire first block is only removed when all of the in-flight // 101 // methods have received their results (so the "methods" list is empty) *AND* // 102 // all of the data written by those methods are visible in the local cache. So // 103 // it is possible for the first block's methods list to be empty, if we are // 104 // still waiting for some objects to quiesce. // 105 // // 106 // Example: // 107 // _outstandingMethodBlocks = [ // 108 // {wait: false, methods: []}, // 109 // {wait: true, methods: [<MethodInvoker for 'login'>]}, // 110 // {wait: false, methods: [<MethodInvoker for 'foo'>, // 111 // <MethodInvoker for 'bar'>]}] // 112 // This means that there were some methods which were sent to the server and // 113 // which have returned their results, but some of the data written by // 114 // the methods may not be visible in the local cache. Once all that data is // 115 // visible, we will send a 'login' method. Once the login method has returned // 116 // and all the data is visible (including re-running subs if userId changes), // 117 // we will send the 'foo' and 'bar' methods in parallel. // 118 self._outstandingMethodBlocks = []; // 119 // 120 // method ID -> array of objects with keys 'collection' and 'id', listing // 121 // documents written by a given method's stub. keys are associated with // 122 // methods whose stub wrote at least one document, and whose data-done message // 123 // has not yet been received. // 124 self._documentsWrittenByStub = {}; // 125 // collection -> IdMap of "server document" object. A "server document" has: // 126 // - "document": the version of the document according the // 127 // server (ie, the snapshot before a stub wrote it, amended by any changes // 128 // received from the server) // 129 // It is undefined if we think the document does not exist // 130 // - "writtenByStubs": a set of method IDs whose stubs wrote to the document // 131 // whose "data done" messages have not yet been processed // 132 self._serverDocuments = {}; // 133 // 134 // Array of callbacks to be called after the next update of the local // 135 // cache. Used for: // 136 // - Calling methodInvoker.dataVisible and sub ready callbacks after // 137 // the relevant data is flushed. // 138 // - Invoking the callbacks of "half-finished" methods after reconnect // 139 // quiescence. Specifically, methods whose result was received over the old // 140 // connection (so we don't re-send it) but whose data had not been made // 141 // visible. // 142 self._afterUpdateCallbacks = []; // 143 // 144 // In two contexts, we buffer all incoming data messages and then process them // 145 // all at once in a single update: // 146 // - During reconnect, we buffer all data messages until all subs that had // 147 // been ready before reconnect are ready again, and all methods that are // 148 // active have returned their "data done message"; then // 149 // - During the execution of a "wait" method, we buffer all data messages // 150 // until the wait method gets its "data done" message. (If the wait method // 151 // occurs during reconnect, it doesn't get any special handling.) // 152 // all data messages are processed in one update. // 153 // // 154 // The following fields are used for this "quiescence" process. // 155 // 156 // This buffers the messages that aren't being processed yet. // 157 self._messagesBufferedUntilQuiescence = []; // 158 // Map from method ID -> true. Methods are removed from this when their // 159 // "data done" message is received, and we will not quiesce until it is // 160 // empty. // 161 self._methodsBlockingQuiescence = {}; // 162 // map from sub ID -> true for subs that were ready (ie, called the sub // 163 // ready callback) before reconnect but haven't become ready again yet // 164 self._subsBeingRevived = {}; // map from sub._id -> true // 165 // if true, the next data update should reset all stores. (set during // 166 // reconnect.) // 167 self._resetStores = false; // 168 // 169 // name -> array of updates for (yet to be created) collections // 170 self._updatesForUnknownStores = {}; // 171 // if we're blocking a migration, the retry func // 172 self._retryMigrate = null; // 173 // 174 // metadata for subscriptions. Map from sub ID to object with keys: // 175 // - id // 176 // - name // 177 // - params // 178 // - inactive (if true, will be cleaned up if not reused in re-run) // 179 // - ready (has the 'ready' message been received?) // 180 // - readyCallback (an optional callback to call when ready) // 181 // - errorCallback (an optional callback to call if the sub terminates with // 182 // an error, XXX COMPAT WITH 1.0.3.1) // 183 // - stopCallback (an optional callback to call when the sub terminates // 184 // for any reason, with an error argument if an error triggered the stop) // 185 self._subscriptions = {}; // 186 // 187 // Reactive userId. // 188 self._userId = null; // 189 self._userIdDeps = new Tracker.Dependency; // 190 // 191 // Block auto-reload while we're waiting for method responses. // 192 if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) { // 193 Package.reload.Reload._onMigrate(function (retry) { // 194 if (!self._readyToMigrate()) { // 195 if (self._retryMigrate) // 196 throw new Error("Two migrations in progress?"); // 197 self._retryMigrate = retry; // 198 return false; // 199 } else { // 200 return [true]; // 201 } // 202 }); // 203 } // 204 // 205 var onMessage = function (raw_msg) { // 206 try { // 207 var msg = parseDDP(raw_msg); // 208 } catch (e) { // 209 Meteor._debug("Exception while parsing DDP", e); // 210 return; // 211 } // 212 // 213 if (msg === null || !msg.msg) { // 214 // XXX COMPAT WITH 0.6.6. ignore the old welcome message for back // 215 // compat. Remove this 'if' once the server stops sending welcome // 216 // messages (stream_server.js). // 217 if (! (msg && msg.server_id)) // 218 Meteor._debug("discarding invalid livedata message", msg); // 219 return; // 220 } // 221 // 222 if (msg.msg === 'connected') { // 223 self._version = self._versionSuggestion; // 224 self._livedata_connected(msg); // 225 options.onConnected(); // 226 } // 227 else if (msg.msg == 'failed') { // 228 if (_.contains(self._supportedDDPVersions, msg.version)) { // 229 self._versionSuggestion = msg.version; // 230 self._stream.reconnect({_force: true}); // 231 } else { // 232 var description = // 233 "DDP version negotiation failed; server requested version " + msg.version; // 234 self._stream.disconnect({_permanent: true, _error: description}); // 235 options.onDDPVersionNegotiationFailure(description); // 236 } // 237 } // 238 else if (msg.msg === 'ping') { // 239 if (options.respondToPings) // 240 self._send({msg: "pong", id: msg.id}); // 241 if (self._heartbeat) // 242 self._heartbeat.pingReceived(); // 243 } // 244 else if (msg.msg === 'pong') { // 245 if (self._heartbeat) { // 246 self._heartbeat.pongReceived(); // 247 } // 248 } // 249 else if (_.include(['added', 'changed', 'removed', 'ready', 'updated'], msg.msg)) // 250 self._livedata_data(msg); // 251 else if (msg.msg === 'nosub') // 252 self._livedata_nosub(msg); // 253 else if (msg.msg === 'result') // 254 self._livedata_result(msg); // 255 else if (msg.msg === 'error') // 256 self._livedata_error(msg); // 257 else // 258 Meteor._debug("discarding unknown livedata message type", msg); // 259 }; // 260 // 261 var onReset = function () { // 262 // Send a connect message at the beginning of the stream. // 263 // NOTE: reset is called even on the first connection, so this is // 264 // the only place we send this message. // 265 var msg = {msg: 'connect'}; // 266 if (self._lastSessionId) // 267 msg.session = self._lastSessionId; // 268 msg.version = self._versionSuggestion || self._supportedDDPVersions[0]; // 269 self._versionSuggestion = msg.version; // 270 msg.support = self._supportedDDPVersions; // 271 self._send(msg); // 272 // 273 // Now, to minimize setup latency, go ahead and blast out all of // 274 // our pending methods ands subscriptions before we've even taken // 275 // the necessary RTT to know if we successfully reconnected. (1) // 276 // They're supposed to be idempotent; (2) even if we did // 277 // reconnect, we're not sure what messages might have gotten lost // 278 // (in either direction) since we were disconnected (TCP being // 279 // sloppy about that.) // 280 // 281 // If the current block of methods all got their results (but didn't all get // 282 // their data visible), discard the empty block now. // 283 if (! _.isEmpty(self._outstandingMethodBlocks) && // 284 _.isEmpty(self._outstandingMethodBlocks[0].methods)) { // 285 self._outstandingMethodBlocks.shift(); // 286 } // 287 // 288 // Mark all messages as unsent, they have not yet been sent on this // 289 // connection. // 290 _.each(self._methodInvokers, function (m) { // 291 m.sentMessage = false; // 292 }); // 293 // 294 // If an `onReconnect` handler is set, call it first. Go through // 295 // some hoops to ensure that methods that are called from within // 296 // `onReconnect` get executed _before_ ones that were originally // 297 // outstanding (since `onReconnect` is used to re-establish auth // 298 // certificates) // 299 if (self.onReconnect) // 300 self._callOnReconnectAndSendAppropriateOutstandingMethods(); // 301 else // 302 self._sendOutstandingMethods(); // 303 // 304 // add new subscriptions at the end. this way they take effect after // 305 // the handlers and we don't see flicker. // 306 _.each(self._subscriptions, function (sub, id) { // 307 self._send({ // 308 msg: 'sub', // 309 id: id, // 310 name: sub.name, // 311 params: sub.params // 312 }); // 313 }); // 314 }; // 315 // 316 var onDisconnect = function () { // 317 if (self._heartbeat) { // 318 self._heartbeat.stop(); // 319 self._heartbeat = null; // 320 } // 321 }; // 322 // 323 if (Meteor.isServer) { // 324 self._stream.on('message', Meteor.bindEnvironment(onMessage, Meteor._debug)); // 325 self._stream.on('reset', Meteor.bindEnvironment(onReset, Meteor._debug)); // 326 self._stream.on('disconnect', Meteor.bindEnvironment(onDisconnect, Meteor._debug)); // 327 } else { // 328 self._stream.on('message', onMessage); // 329 self._stream.on('reset', onReset); // 330 self._stream.on('disconnect', onDisconnect); // 331 } // 332 }; // 333 // 334 // A MethodInvoker manages sending a method to the server and calling the user's // 335 // callbacks. On construction, it registers itself in the connection's // 336 // _methodInvokers map; it removes itself once the method is fully finished and // 337 // the callback is invoked. This occurs when it has both received a result, // 338 // and the data written by it is fully visible. // 339 var MethodInvoker = function (options) { // 340 var self = this; // 341 // 342 // Public (within this file) fields. // 343 self.methodId = options.methodId; // 344 self.sentMessage = false; // 345 // 346 self._callback = options.callback; // 347 self._connection = options.connection; // 348 self._message = options.message; // 349 self._onResultReceived = options.onResultReceived || function () {}; // 350 self._wait = options.wait; // 351 self._methodResult = null; // 352 self._dataVisible = false; // 353 // 354 // Register with the connection. // 355 self._connection._methodInvokers[self.methodId] = self; // 356 }; // 357 _.extend(MethodInvoker.prototype, { // 358 // Sends the method message to the server. May be called additional times if // 359 // we lose the connection and reconnect before receiving a result. // 360 sendMessage: function () { // 361 var self = this; // 362 // This function is called before sending a method (including resending on // 363 // reconnect). We should only (re)send methods where we don't already have a // 364 // result! // 365 if (self.gotResult()) // 366 throw new Error("sendingMethod is called on method with result"); // 367 // 368 // If we're re-sending it, it doesn't matter if data was written the first // 369 // time. // 370 self._dataVisible = false; // 371 // 372 self.sentMessage = true; // 373 // 374 // If this is a wait method, make all data messages be buffered until it is // 375 // done. // 376 if (self._wait) // 377 self._connection._methodsBlockingQuiescence[self.methodId] = true; // 378 // 379 // Actually send the message. // 380 self._connection._send(self._message); // 381 }, // 382 // Invoke the callback, if we have both a result and know that all data has // 383 // been written to the local cache. // 384 _maybeInvokeCallback: function () { // 385 var self = this; // 386 if (self._methodResult && self._dataVisible) { // 387 // Call the callback. (This won't throw: the callback was wrapped with // 388 // bindEnvironment.) // 389 self._callback(self._methodResult[0], self._methodResult[1]); // 390 // 391 // Forget about this method. // 392 delete self._connection._methodInvokers[self.methodId]; // 393 // 394 // Let the connection know that this method is finished, so it can try to // 395 // move on to the next block of methods. // 396 self._connection._outstandingMethodFinished(); // 397 } // 398 }, // 399 // Call with the result of the method from the server. Only may be called // 400 // once; once it is called, you should not call sendMessage again. // 401 // If the user provided an onResultReceived callback, call it immediately. // 402 // Then invoke the main callback if data is also visible. // 403 receiveResult: function (err, result) { // 404 var self = this; // 405 if (self.gotResult()) // 406 throw new Error("Methods should only receive results once"); // 407 self._methodResult = [err, result]; // 408 self._onResultReceived(err, result); // 409 self._maybeInvokeCallback(); // 410 }, // 411 // Call this when all data written by the method is visible. This means that // 412 // the method has returns its "data is done" message *AND* all server // 413 // documents that are buffered at that time have been written to the local // 414 // cache. Invokes the main callback if the result has been received. // 415 dataVisible: function () { // 416 var self = this; // 417 self._dataVisible = true; // 418 self._maybeInvokeCallback(); // 419 }, // 420 // True if receiveResult has been called. // 421 gotResult: function () { // 422 var self = this; // 423 return !!self._methodResult; // 424 } // 425 }); // 426 // 427 _.extend(Connection.prototype, { // 428 // 'name' is the name of the data on the wire that should go in the // 429 // store. 'wrappedStore' should be an object with methods beginUpdate, update, // 430 // endUpdate, saveOriginals, retrieveOriginals. see Collection for an example. // 431 registerStore: function (name, wrappedStore) { // 432 var self = this; // 433 // 434 if (name in self._stores) // 435 return false; // 436 // 437 // Wrap the input object in an object which makes any store method not // 438 // implemented by 'store' into a no-op. // 439 var store = {}; // 440 _.each(['update', 'beginUpdate', 'endUpdate', 'saveOriginals', // 441 'retrieveOriginals'], function (method) { // 442 store[method] = function () { // 443 return (wrappedStore[method] // 444 ? wrappedStore[method].apply(wrappedStore, arguments) // 445 : undefined); // 446 }; // 447 }); // 448 // 449 self._stores[name] = store; // 450 // 451 var queued = self._updatesForUnknownStores[name]; // 452 if (queued) { // 453 store.beginUpdate(queued.length, false); // 454 _.each(queued, function (msg) { // 455 store.update(msg); // 456 }); // 457 store.endUpdate(); // 458 delete self._updatesForUnknownStores[name]; // 459 } // 460 // 461 return true; // 462 }, // 463 // 464 /** // 465 * @memberOf Meteor // 466 * @summary Subscribe to a record set. Returns a handle that provides // 467 * `stop()` and `ready()` methods. // 468 * @locus Client // 469 * @param {String} name Name of the subscription. Matches the name of the // 470 * server's `publish()` call. // 471 * @param {Any} [arg1,arg2...] Optional arguments passed to publisher // 472 * function on server. // 473 * @param {Function|Object} [callbacks] Optional. May include `onStop` // 474 * and `onReady` callbacks. If there is an error, it is passed as an // 475 * argument to `onStop`. If a function is passed instead of an object, it // 476 * is interpreted as an `onReady` callback. // 477 */ // 478 subscribe: function (name /* .. [arguments] .. (callback|callbacks) */) { // 479 var self = this; // 480 // 481 var params = Array.prototype.slice.call(arguments, 1); // 482 var callbacks = {}; // 483 if (params.length) { // 484 var lastParam = params[params.length - 1]; // 485 if (_.isFunction(lastParam)) { // 486 callbacks.onReady = params.pop(); // 487 } else if (lastParam && // 488 // XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use // 489 // onStop with an error callback instead. // 490 _.any([lastParam.onReady, lastParam.onError, lastParam.onStop], // 491 _.isFunction)) { // 492 callbacks = params.pop(); // 493 } // 494 } // 495 // 496 // Is there an existing sub with the same name and param, run in an // 497 // invalidated Computation? This will happen if we are rerunning an // 498 // existing computation. // 499 // // 500 // For example, consider a rerun of: // 501 // // 502 // Tracker.autorun(function () { // 503 // Meteor.subscribe("foo", Session.get("foo")); // 504 // Meteor.subscribe("bar", Session.get("bar")); // 505 // }); // 506 // // 507 // If "foo" has changed but "bar" has not, we will match the "bar" // 508 // subcribe to an existing inactive subscription in order to not // 509 // unsub and resub the subscription unnecessarily. // 510 // // 511 // We only look for one such sub; if there are N apparently-identical subs // 512 // being invalidated, we will require N matching subscribe calls to keep // 513 // them all active. // 514 var existing = _.find(self._subscriptions, function (sub) { // 515 return sub.inactive && sub.name === name && // 516 EJSON.equals(sub.params, params); // 517 }); // 518 // 519 var id; // 520 if (existing) { // 521 id = existing.id; // 522 existing.inactive = false; // reactivate // 523 // 524 if (callbacks.onReady) { // 525 // If the sub is not already ready, replace any ready callback with the // 526 // one provided now. (It's not really clear what users would expect for // 527 // an onReady callback inside an autorun; the semantics we provide is // 528 // that at the time the sub first becomes ready, we call the last // 529 // onReady callback provided, if any.) // 530 if (!existing.ready) // 531 existing.readyCallback = callbacks.onReady; // 532 } // 533 // 534 // XXX COMPAT WITH 1.0.3.1 we used to have onError but now we call // 535 // onStop with an optional error argument // 536 if (callbacks.onError) { // 537 // Replace existing callback if any, so that errors aren't // 538 // double-reported. // 539 existing.errorCallback = callbacks.onError; // 540 } // 541 // 542 if (callbacks.onStop) { // 543 existing.stopCallback = callbacks.onStop; // 544 } // 545 } else { // 546 // New sub! Generate an id, save it locally, and send message. // 547 id = Random.id(); // 548 self._subscriptions[id] = { // 549 id: id, // 550 name: name, // 551 params: EJSON.clone(params), // 552 inactive: false, // 553 ready: false, // 554 readyDeps: new Tracker.Dependency, // 555 readyCallback: callbacks.onReady, // 556 // XXX COMPAT WITH 1.0.3.1 #errorCallback // 557 errorCallback: callbacks.onError, // 558 stopCallback: callbacks.onStop, // 559 connection: self, // 560 remove: function() { // 561 delete this.connection._subscriptions[this.id]; // 562 this.ready && this.readyDeps.changed(); // 563 }, // 564 stop: function() { // 565 this.connection._send({msg: 'unsub', id: id}); // 566 this.remove(); // 567 // 568 if (callbacks.onStop) { // 569 callbacks.onStop(); // 570 } // 571 } // 572 }; // 573 self._send({msg: 'sub', id: id, name: name, params: params}); // 574 } // 575 // 576 // return a handle to the application. // 577 var handle = { // 578 stop: function () { // 579 if (!_.has(self._subscriptions, id)) // 580 return; // 581 // 582 self._subscriptions[id].stop(); // 583 }, // 584 ready: function () { // 585 // return false if we've unsubscribed. // 586 if (!_.has(self._subscriptions, id)) // 587 return false; // 588 var record = self._subscriptions[id]; // 589 record.readyDeps.depend(); // 590 return record.ready; // 591 }, // 592 subscriptionId: id // 593 }; // 594 // 595 if (Tracker.active) { // 596 // We're in a reactive computation, so we'd like to unsubscribe when the // 597 // computation is invalidated... but not if the rerun just re-subscribes // 598 // to the same subscription! When a rerun happens, we use onInvalidate // 599 // as a change to mark the subscription "inactive" so that it can // 600 // be reused from the rerun. If it isn't reused, it's killed from // 601 // an afterFlush. // 602 Tracker.onInvalidate(function (c) { // 603 if (_.has(self._subscriptions, id)) // 604 self._subscriptions[id].inactive = true; // 605 // 606 Tracker.afterFlush(function () { // 607 if (_.has(self._subscriptions, id) && // 608 self._subscriptions[id].inactive) // 609 handle.stop(); // 610 }); // 611 }); // 612 } // 613 // 614 return handle; // 615 }, // 616 // 617 // options: // 618 // - onLateError {Function(error)} called if an error was received after the ready event. // 619 // (errors received before ready cause an error to be thrown) // 620 _subscribeAndWait: function (name, args, options) { // 621 var self = this; // 622 var f = new Future(); // 623 var ready = false; // 624 var handle; // 625 args = args || []; // 626 args.push({ // 627 onReady: function () { // 628 ready = true; // 629 f['return'](); // 630 }, // 631 onError: function (e) { // 632 if (!ready) // 633 f['throw'](e); // 634 else // 635 options && options.onLateError && options.onLateError(e); // 636 } // 637 }); // 638 // 639 handle = self.subscribe.apply(self, [name].concat(args)); // 640 f.wait(); // 641 return handle; // 642 }, // 643 // 644 methods: function (methods) { // 645 var self = this; // 646 _.each(methods, function (func, name) { // 647 if (self._methodHandlers[name]) // 648 throw new Error("A method named '" + name + "' is already defined"); // 649 self._methodHandlers[name] = func; // 650 }); // 651 }, // 652 // 653 /** // 654 * @memberOf Meteor // 655 * @summary Invokes a method passing any number of arguments. // 656 * @locus Anywhere // 657 * @param {String} name Name of method to invoke // 658 * @param {EJSONable} [arg1,arg2...] Optional method arguments // 659 * @param {Function} [asyncCallback] Optional callback, which is called asynchronously with the error or result after the method is complete. If not provided, the method runs synchronously if possible (see below). */ // 661 call: function (name /* .. [arguments] .. callback */) { // 662 // if it's a function, the last argument is the result callback, // 663 // not a parameter to the remote method. // 664 var args = Array.prototype.slice.call(arguments, 1); // 665 if (args.length && typeof args[args.length - 1] === "function") // 666 var callback = args.pop(); // 667 return this.apply(name, args, callback); // 668 }, // 669 // 670 // @param options {Optional Object} // 671 // wait: Boolean - Should we wait to call this until all current methods // 672 // are fully finished, and block subsequent method calls // 673 // until this method is fully finished? // 674 // (does not affect methods called from within this method) // 675 // onResultReceived: Function - a callback to call as soon as the method // 676 // result is received. the data written by // 677 // the method may not yet be in the cache! // 678 // returnStubValue: Boolean - If true then in cases where we would have // 679 // otherwise discarded the stub's return value // 680 // and returned undefined, instead we go ahead // 681 // and return it. Specifically, this is any // 682 // time other than when (a) we are already // 683 // inside a stub or (b) we are in Node and no // 684 // callback was provided. Currently we require // 685 // this flag to be explicitly passed to reduce // 686 // the likelihood that stub return values will // 687 // be confused with server return values; we // 688 // may improve this in future. // 689 // @param callback {Optional Function} // 690 // 691 /** // 692 * @memberOf Meteor // 693 * @summary Invoke a method passing an array of arguments. // 694 * @locus Anywhere // 695 * @param {String} name Name of method to invoke // 696 * @param {EJSONable[]} args Method arguments // 697 * @param {Object} [options] // 698 * @param {Boolean} options.wait (Client only) If true, don't send this method until all previous method calls have completed, and don't send any subsequent method calls until this one is completed. * @param {Function} options.onResultReceived (Client only) This callback is invoked with the error or result of the method (just like `asyncCallback`) as soon as the error or result is available. The local cache may not yet reflect the writes performed by the method. * @param {Function} [asyncCallback] Optional callback; same semantics as in [`Meteor.call`](#meteor_call). // 701 */ // 702 apply: function (name, args, options, callback) { // 703 var self = this; // 704 // 705 // We were passed 3 arguments. They may be either (name, args, options) // 706 // or (name, args, callback) // 707 if (!callback && typeof options === 'function') { // 708 callback = options; // 709 options = {}; // 710 } // 711 options = options || {}; // 712 // 713 if (callback) { // 714 // XXX would it be better form to do the binding in stream.on, // 715 // or caller, instead of here? // 716 // XXX improve error message (and how we report it) // 717 callback = Meteor.bindEnvironment( // 718 callback, // 719 "delivering result of invoking '" + name + "'" // 720 ); // 721 } // 722 // 723 // Keep our args safe from mutation (eg if we don't send the message for a // 724 // while because of a wait method). // 725 args = EJSON.clone(args); // 726 // 727 // Lazily allocate method ID once we know that it'll be needed. // 728 var methodId = (function () { // 729 var id; // 730 return function () { // 731 if (id === undefined) // 732 id = '' + (self._nextMethodId++); // 733 return id; // 734 }; // 735 })(); // 736 // 737 var enclosing = DDP._CurrentInvocation.get(); // 738 var alreadyInSimulation = enclosing && enclosing.isSimulation; // 739 // 740 // Lazily generate a randomSeed, only if it is requested by the stub. // 741 // The random streams only have utility if they're used on both the client // 742 // and the server; if the client doesn't generate any 'random' values // 743 // then we don't expect the server to generate any either. // 744 // Less commonly, the server may perform different actions from the client, // 745 // and may in fact generate values where the client did not, but we don't // 746 // have any client-side values to match, so even here we may as well just // 747 // use a random seed on the server. In that case, we don't pass the // 748 // randomSeed to save bandwidth, and we don't even generate it to save a // 749 // bit of CPU and to avoid consuming entropy. // 750 var randomSeed = null; // 751 var randomSeedGenerator = function () { // 752 if (randomSeed === null) { // 753 randomSeed = makeRpcSeed(enclosing, name); // 754 } // 755 return randomSeed; // 756 }; // 757 // 758 // Run the stub, if we have one. The stub is supposed to make some // 759 // temporary writes to the database to give the user a smooth experience // 760 // until the actual result of executing the method comes back from the // 761 // server (whereupon the temporary writes to the database will be reversed // 762 // during the beginUpdate/endUpdate process.) // 763 // // 764 // Normally, we ignore the return value of the stub (even if it is an // 765 // exception), in favor of the real return value from the server. The // 766 // exception is if the *caller* is a stub. In that case, we're not going // 767 // to do a RPC, so we use the return value of the stub as our return // 768 // value. // 769 // 770 var stub = self._methodHandlers[name]; // 771 if (stub) { // 772 var setUserId = function(userId) { // 773 self.setUserId(userId); // 774 }; // 775 // 776 var invocation = new MethodInvocation({ // 777 isSimulation: true, // 778 userId: self.userId(), // 779 setUserId: setUserId, // 780 randomSeed: function () { return randomSeedGenerator(); } // 781 }); // 782 // 783 if (!alreadyInSimulation) // 784 self._saveOriginals(); // 785 // 786 try { // 787 // Note that unlike in the corresponding server code, we never audit // 788 // that stubs check() their arguments. // 789 var stubReturnValue = DDP._CurrentInvocation.withValue(invocation, function () { // 790 if (Meteor.isServer) { // 791 // Because saveOriginals and retrieveOriginals aren't reentrant, // 792 // don't allow stubs to yield. // 793 return Meteor._noYieldsAllowed(function () { // 794 // re-clone, so that the stub can't affect our caller's values // 795 return stub.apply(invocation, EJSON.clone(args)); // 796 }); // 797 } else { // 798 return stub.apply(invocation, EJSON.clone(args)); // 799 } // 800 }); // 801 } // 802 catch (e) { // 803 var exception = e; // 804 } // 805 // 806 if (!alreadyInSimulation) // 807 self._retrieveAndStoreOriginals(methodId()); // 808 } // 809 // 810 // If we're in a simulation, stop and return the result we have, // 811 // rather than going on to do an RPC. If there was no stub, // 812 // we'll end up returning undefined. // 813 if (alreadyInSimulation) { // 814 if (callback) { // 815 callback(exception, stubReturnValue); // 816 return undefined; // 817 } // 818 if (exception) // 819 throw exception; // 820 return stubReturnValue; // 821 } // 822 // 823 // If an exception occurred in a stub, and we're ignoring it // 824 // because we're doing an RPC and want to use what the server // 825 // returns instead, log it so the developer knows. // 826 // // 827 // Tests can set the 'expected' flag on an exception so it won't // 828 // go to log. // 829 if (exception && !exception.expected) { // 830 Meteor._debug("Exception while simulating the effect of invoking '" + // 831 name + "'", exception, exception.stack); // 832 } // 833 // 834 // 835 // At this point we're definitely doing an RPC, and we're going to // 836 // return the value of the RPC to the caller. // 837 // 838 // If the caller didn't give a callback, decide what to do. // 839 if (!callback) { // 840 if (Meteor.isClient) { // 841 // On the client, we don't have fibers, so we can't block. The // 842 // only thing we can do is to return undefined and discard the // 843 // result of the RPC. If an error occurred then print the error // 844 // to the console. // 845 callback = function (err) { // 846 err && Meteor._debug("Error invoking Method '" + name + "':", // 847 err.message); // 848 }; // 849 } else { // 850 // On the server, make the function synchronous. Throw on // 851 // errors, return on success. // 852 var future = new Future; // 853 callback = future.resolver(); // 854 } // 855 } // 856 // Send the RPC. Note that on the client, it is important that the // 857 // stub have finished before we send the RPC, so that we know we have // 858 // a complete list of which local documents the stub wrote. // 859 var message = { // 860 msg: 'method', // 861 method: name, // 862 params: args, // 863 id: methodId() // 864 }; // 865 // 866 // Send the randomSeed only if we used it // 867 if (randomSeed !== null) { // 868 message.randomSeed = randomSeed; // 869 } // 870 // 871 var methodInvoker = new MethodInvoker({ // 872 methodId: methodId(), // 873 callback: callback, // 874 connection: self, // 875 onResultReceived: options.onResultReceived, // 876 wait: !!options.wait, // 877 message: message // 878 }); // 879 // 880 if (options.wait) { // 881 // It's a wait method! Wait methods go in their own block. // 882 self._outstandingMethodBlocks.push( // 883 {wait: true, methods: [methodInvoker]}); // 884 } else { // 885 // Not a wait method. Start a new block if the previous block was a wait // 886 // block, and add it to the last block of methods. // 887 if (_.isEmpty(self._outstandingMethodBlocks) || // 888 _.last(self._outstandingMethodBlocks).wait) // 889 self._outstandingMethodBlocks.push({wait: false, methods: []}); // 890 _.last(self._outstandingMethodBlocks).methods.push(methodInvoker); // 891 } // 892 // 893 // If we added it to the first block, send it out now. // 894 if (self._outstandingMethodBlocks.length === 1) // 895 methodInvoker.sendMessage(); // 896 // 897 // If we're using the default callback on the server, // 898 // block waiting for the result. // 899 if (future) { // 900 return future.wait(); // 901 } // 902 return options.returnStubValue ? stubReturnValue : undefined; // 903 }, // 904 // 905 // Before calling a method stub, prepare all stores to track changes and allow // 906 // _retrieveAndStoreOriginals to get the original versions of changed // 907 // documents. // 908 _saveOriginals: function () { // 909 var self = this; // 910 _.each(self._stores, function (s) { // 911 s.saveOriginals(); // 912 }); // 913 }, // 914 // Retrieves the original versions of all documents modified by the stub for // 915 // method 'methodId' from all stores and saves them to _serverDocuments (keyed // 916 // by document) and _documentsWrittenByStub (keyed by method ID). // 917 _retrieveAndStoreOriginals: function (methodId) { // 918 var self = this; // 919 if (self._documentsWrittenByStub[methodId]) // 920 throw new Error("Duplicate methodId in _retrieveAndStoreOriginals"); // 921 // 922 var docsWritten = []; // 923 _.each(self._stores, function (s, collection) { // 924 var originals = s.retrieveOriginals(); // 925 // not all stores define retrieveOriginals // 926 if (!originals) // 927 return; // 928 originals.forEach(function (doc, id) { // 929 docsWritten.push({collection: collection, id: id}); // 930 if (!_.has(self._serverDocuments, collection)) // 931 self._serverDocuments[collection] = new LocalCollection._IdMap; // 932 var serverDoc = self._serverDocuments[collection].setDefault(id, {}); // 933 if (serverDoc.writtenByStubs) { // 934 // We're not the first stub to write this doc. Just add our method ID // 935 // to the record. // 936 serverDoc.writtenByStubs[methodId] = true; // 937 } else { // 938 // First stub! Save the original value and our method ID. // 939 serverDoc.document = doc; // 940 serverDoc.flushCallbacks = []; // 941 serverDoc.writtenByStubs = {}; // 942 serverDoc.writtenByStubs[methodId] = true; // 943 } // 944 }); // 945 }); // 946 if (!_.isEmpty(docsWritten)) { // 947 self._documentsWrittenByStub[methodId] = docsWritten; // 948 } // 949 }, // 950 // 951 // This is very much a private function we use to make the tests // 952 // take up fewer server resources after they complete. // 953 _unsubscribeAll: function () { // 954 var self = this; // 955 _.each(_.clone(self._subscriptions), function (sub, id) { // 956 // Avoid killing the autoupdate subscription so that developers // 957 // still get hot code pushes when writing tests. // 958 // // 959 // XXX it's a hack to encode knowledge about autoupdate here, // 960 // but it doesn't seem worth it yet to have a special API for // 961 // subscriptions to preserve after unit tests. // 962 if (sub.name !== 'meteor_autoupdate_clientVersions') { // 963 self._subscriptions[id].stop(); // 964 } // 965 }); // 966 }, // 967 // 968 // Sends the DDP stringification of the given message object // 969 _send: function (obj) { // 970 var self = this; // 971 self._stream.send(stringifyDDP(obj)); // 972 }, // 973 // 974 // We detected via DDP-level heartbeats that we've lost the // 975 // connection. Unlike `disconnect` or `close`, a lost connection // 976 // will be automatically retried. // 977 _lostConnection: function (error) { // 978 var self = this; // 979 self._stream._lostConnection(error); // 980 }, // 981 // 982 /** // 983 * @summary Get the current connection status. A reactive data source. // 984 * @locus Client // 985 * @memberOf Meteor // 986 */ // 987 status: function (/*passthrough args*/) { // 988 var self = this; // 989 return self._stream.status.apply(self._stream, arguments); // 990 }, // 991 // 992 /** // 993 * @summary Force an immediate reconnection attempt if the client is not connected to the server. // 994 // 995 This method does nothing if the client is already connected. // 996 * @locus Client // 997 * @memberOf Meteor // 998 */ // 999 reconnect: function (/*passthrough args*/) { // 1000 var self = this; // 1001 return self._stream.reconnect.apply(self._stream, arguments); // 1002 }, // 1003 // 1004 /** // 1005 * @summary Disconnect the client from the server. // 1006 * @locus Client // 1007 * @memberOf Meteor // 1008 */ // 1009 disconnect: function (/*passthrough args*/) { // 1010 var self = this; // 1011 return self._stream.disconnect.apply(self._stream, arguments); // 1012 }, // 1013 // 1014 close: function () { // 1015 var self = this; // 1016 return self._stream.disconnect({_permanent: true}); // 1017 }, // 1018 // 1019 /// // 1020 /// Reactive user system // 1021 /// // 1022 userId: function () { // 1023 var self = this; // 1024 if (self._userIdDeps) // 1025 self._userIdDeps.depend(); // 1026 return self._userId; // 1027 }, // 1028 // 1029 setUserId: function (userId) { // 1030 var self = this; // 1031 // Avoid invalidating dependents if setUserId is called with current value. // 1032 if (self._userId === userId) // 1033 return; // 1034 self._userId = userId; // 1035 if (self._userIdDeps) // 1036 self._userIdDeps.changed(); // 1037 }, // 1038 // 1039 // Returns true if we are in a state after reconnect of waiting for subs to be // 1040 // revived or early methods to finish their data, or we are waiting for a // 1041 // "wait" method to finish. // 1042 _waitingForQuiescence: function () { // 1043 var self = this; // 1044 return (! _.isEmpty(self._subsBeingRevived) || // 1045 ! _.isEmpty(self._methodsBlockingQuiescence)); // 1046 }, // 1047 // 1048 // Returns true if any method whose message has been sent to the server has // 1049 // not yet invoked its user callback. // 1050 _anyMethodsAreOutstanding: function () { // 1051 var self = this; // 1052 return _.any(_.pluck(self._methodInvokers, 'sentMessage')); // 1053 }, // 1054 // 1055 _livedata_connected: function (msg) { // 1056 var self = this; // 1057 // 1058 if (self._version !== 'pre1' && self._heartbeatInterval !== 0) { // 1059 self._heartbeat = new Heartbeat({ // 1060 heartbeatInterval: self._heartbeatInterval, // 1061 heartbeatTimeout: self._heartbeatTimeout, // 1062 onTimeout: function () { // 1063 self._lostConnection( // 1064 new DDP.ConnectionError("DDP heartbeat timed out")); // 1065 }, // 1066 sendPing: function () { // 1067 self._send({msg: 'ping'}); // 1068 } // 1069 }); // 1070 self._heartbeat.start(); // 1071 } // 1072 // 1073 // If this is a reconnect, we'll have to reset all stores. // 1074 if (self._lastSessionId) // 1075 self._resetStores = true; // 1076 // 1077 if (typeof (msg.session) === "string") { // 1078 var reconnectedToPreviousSession = (self._lastSessionId === msg.session); // 1079 self._lastSessionId = msg.session; // 1080 } // 1081 // 1082 if (reconnectedToPreviousSession) { // 1083 // Successful reconnection -- pick up where we left off. Note that right // 1084 // now, this never happens: the server never connects us to a previous // 1085 // session, because DDP doesn't provide enough data for the server to know // 1086 // what messages the client has processed. We need to improve DDP to make // 1087 // this possible, at which point we'll probably need more code here. // 1088 return; // 1089 } // 1090 // 1091 // Server doesn't have our data any more. Re-sync a new session. // 1092 // 1093 // Forget about messages we were buffering for unknown collections. They'll // 1094 // be resent if still relevant. // 1095 self._updatesForUnknownStores = {}; // 1096 // 1097 if (self._resetStores) { // 1098 // Forget about the effects of stubs. We'll be resetting all collections // 1099 // anyway. // 1100 self._documentsWrittenByStub = {}; // 1101 self._serverDocuments = {}; // 1102 } // 1103 // 1104 // Clear _afterUpdateCallbacks. // 1105 self._afterUpdateCallbacks = []; // 1106 // 1107 // Mark all named subscriptions which are ready (ie, we already called the // 1108 // ready callback) as needing to be revived. // 1109 // XXX We should also block reconnect quiescence until unnamed subscriptions // 1110 // (eg, autopublish) are done re-publishing to avoid flicker! // 1111 self._subsBeingRevived = {}; // 1112 _.each(self._subscriptions, function (sub, id) { // 1113 if (sub.ready) // 1114 self._subsBeingRevived[id] = true; // 1115 }); // 1116 // 1117 // Arrange for "half-finished" methods to have their callbacks run, and // 1118 // track methods that were sent on this connection so that we don't // 1119 // quiesce until they are all done. // 1120 // // 1121 // Start by clearing _methodsBlockingQuiescence: methods sent before // 1122 // reconnect don't matter, and any "wait" methods sent on the new connection // 1123 // that we drop here will be restored by the loop below. // 1124 self._methodsBlockingQuiescence = {}; // 1125 if (self._resetStores) { // 1126 _.each(self._methodInvokers, function (invoker) { // 1127 if (invoker.gotResult()) { // 1128 // This method already got its result, but it didn't call its callback // 1129 // because its data didn't become visible. We did not resend the // 1130 // method RPC. We'll call its callback when we get a full quiesce, // 1131 // since that's as close as we'll get to "data must be visible". // 1132 self._afterUpdateCallbacks.push(_.bind(invoker.dataVisible, invoker)); // 1133 } else if (invoker.sentMessage) { // 1134 // This method has been sent on this connection (maybe as a resend // 1135 // from the last connection, maybe from onReconnect, maybe just very // 1136 // quickly before processing the connected message). // 1137 // // 1138 // We don't need to do anything special to ensure its callbacks get // 1139 // called, but we'll count it as a method which is preventing // 1140 // reconnect quiescence. (eg, it might be a login method that was run // 1141 // from onReconnect, and we don't want to see flicker by seeing a // 1142 // logged-out state.) // 1143 self._methodsBlockingQuiescence[invoker.methodId] = true; // 1144 } // 1145 }); // 1146 } // 1147 // 1148 self._messagesBufferedUntilQuiescence = []; // 1149 // 1150 // If we're not waiting on any methods or subs, we can reset the stores and // 1151 // call the callbacks immediately. // 1152 if (!self._waitingForQuiescence()) { // 1153 if (self._resetStores) { // 1154 _.each(self._stores, function (s) { // 1155 s.beginUpdate(0, true); // 1156 s.endUpdate(); // 1157 }); // 1158 self._resetStores = false; // 1159 } // 1160 self._runAfterUpdateCallbacks(); // 1161 } // 1162 }, // 1163 // 1164 // 1165 _processOneDataMessage: function (msg, updates) { // 1166 var self = this; // 1167 // Using underscore here so as not to need to capitalize. // 1168 self['_process_' + msg.msg](msg, updates); // 1169 }, // 1170 // 1171 // 1172 _livedata_data: function (msg) { // 1173 var self = this; // 1174 // 1175 // collection name -> array of messages // 1176 var updates = {}; // 1177 // 1178 if (self._waitingForQuiescence()) { // 1179 self._messagesBufferedUntilQuiescence.push(msg); // 1180 // 1181 if (msg.msg === "nosub") // 1182 delete self._subsBeingRevived[msg.id]; // 1183 // 1184 _.each(msg.subs || [], function (subId) { // 1185 delete self._subsBeingRevived[subId]; // 1186 }); // 1187 _.each(msg.methods || [], function (methodId) { // 1188 delete self._methodsBlockingQuiescence[methodId]; // 1189 }); // 1190 // 1191 if (self._waitingForQuiescence()) // 1192 return; // 1193 // 1194 // No methods or subs are blocking quiescence! // 1195 // We'll now process and all of our buffered messages, reset all stores, // 1196 // and apply them all at once. // 1197 _.each(self._messagesBufferedUntilQuiescence, function (bufferedMsg) { // 1198 self._processOneDataMessage(bufferedMsg, updates); // 1199 }); // 1200 self._messagesBufferedUntilQuiescence = []; // 1201 } else { // 1202 self._processOneDataMessage(msg, updates); // 1203 } // 1204 // 1205 if (self._resetStores || !_.isEmpty(updates)) { // 1206 // Begin a transactional update of each store. // 1207 _.each(self._stores, function (s, storeName) { // 1208 s.beginUpdate(_.has(updates, storeName) ? updates[storeName].length : 0, // 1209 self._resetStores); // 1210 }); // 1211 self._resetStores = false; // 1212 // 1213 _.each(updates, function (updateMessages, storeName) { // 1214 var store = self._stores[storeName]; // 1215 if (store) { // 1216 _.each(updateMessages, function (updateMessage) { // 1217 store.update(updateMessage); // 1218 }); // 1219 } else { // 1220 // Nobody's listening for this data. Queue it up until // 1221 // someone wants it. // 1222 // XXX memory use will grow without bound if you forget to // 1223 // create a collection or just don't care about it... going // 1224 // to have to do something about that. // 1225 if (!_.has(self._updatesForUnknownStores, storeName)) // 1226 self._updatesForUnknownStores[storeName] = []; // 1227 Array.prototype.push.apply(self._updatesForUnknownStores[storeName], // 1228 updateMessages); // 1229 } // 1230 }); // 1231 // 1232 // End update transaction. // 1233 _.each(self._stores, function (s) { s.endUpdate(); }); // 1234 } // 1235 // 1236 self._runAfterUpdateCallbacks(); // 1237 }, // 1238 // 1239 // Call any callbacks deferred with _runWhenAllServerDocsAreFlushed whose // 1240 // relevant docs have been flushed, as well as dataVisible callbacks at // 1241 // reconnect-quiescence time. // 1242 _runAfterUpdateCallbacks: function () { // 1243 var self = this; // 1244 var callbacks = self._afterUpdateCallbacks; // 1245 self._afterUpdateCallbacks = []; // 1246 _.each(callbacks, function (c) { // 1247 c(); // 1248 }); // 1249 }, // 1250 // 1251 _pushUpdate: function (updates, collection, msg) { // 1252 var self = this; // 1253 if (!_.has(updates, collection)) { // 1254 updates[collection] = []; // 1255 } // 1256 updates[collection].push(msg); // 1257 }, // 1258 // 1259 _getServerDoc: function (collection, id) { // 1260 var self = this; // 1261 if (!_.has(self._serverDocuments, collection)) // 1262 return null; // 1263 var serverDocsForCollection = self._serverDocuments[collection]; // 1264 return serverDocsForCollection.get(id) || null; // 1265 }, // 1266 // 1267 _process_added: function (msg, updates) { // 1268 var self = this; // 1269 var id = LocalCollection._idParse(msg.id); // 1270 var serverDoc = self._getServerDoc(msg.collection, id); // 1271 if (serverDoc) { // 1272 // Some outstanding stub wrote here. // 1273 if (serverDoc.document !== undefined) // 1274 throw new Error("Server sent add for existing id: " + msg.id); // 1275 serverDoc.document = msg.fields || {}; // 1276 serverDoc.document._id = id; // 1277 } else { // 1278 self._pushUpdate(updates, msg.collection, msg); // 1279 } // 1280 }, // 1281 // 1282 _process_changed: function (msg, updates) { // 1283 var self = this; // 1284 var serverDoc = self._getServerDoc( // 1285 msg.collection, LocalCollection._idParse(msg.id)); // 1286 if (serverDoc) { // 1287 if (serverDoc.document === undefined) // 1288 throw new Error("Server sent changed for nonexisting id: " + msg.id); // 1289 LocalCollection._applyChanges(serverDoc.document, msg.fields); // 1290 } else { // 1291 self._pushUpdate(updates, msg.collection, msg); // 1292 } // 1293 }, // 1294 // 1295 _process_removed: function (msg, updates) { // 1296 var self = this; // 1297 var serverDoc = self._getServerDoc( // 1298 msg.collection, LocalCollection._idParse(msg.id)); // 1299 if (serverDoc) { // 1300 // Some outstanding stub wrote here. // 1301 if (serverDoc.document === undefined) // 1302 throw new Error("Server sent removed for nonexisting id:" + msg.id); // 1303 serverDoc.document = undefined; // 1304 } else { // 1305 self._pushUpdate(updates, msg.collection, { // 1306 msg: 'removed', // 1307 collection: msg.collection, // 1308 id: msg.id // 1309 }); // 1310 } // 1311 }, // 1312 // 1313 _process_updated: function (msg, updates) { // 1314 var self = this; // 1315 // Process "method done" messages. // 1316 _.each(msg.methods, function (methodId) { // 1317 _.each(self._documentsWrittenByStub[methodId], function (written) { // 1318 var serverDoc = self._getServerDoc(written.collection, written.id); // 1319 if (!serverDoc) // 1320 throw new Error("Lost serverDoc for " + JSON.stringify(written)); // 1321 if (!serverDoc.writtenByStubs[methodId]) // 1322 throw new Error("Doc " + JSON.stringify(written) + // 1323 " not written by method " + methodId); // 1324 delete serverDoc.writtenByStubs[methodId]; // 1325 if (_.isEmpty(serverDoc.writtenByStubs)) { // 1326 // All methods whose stubs wrote this method have completed! We can // 1327 // now copy the saved document to the database (reverting the stub's // 1328 // change if the server did not write to this object, or applying the // 1329 // server's writes if it did). // 1330 // 1331 // This is a fake ddp 'replace' message. It's just for talking // 1332 // between livedata connections and minimongo. (We have to stringify // 1333 // the ID because it's supposed to look like a wire message.) // 1334 self._pushUpdate(updates, written.collection, { // 1335 msg: 'replace', // 1336 id: LocalCollection._idStringify(written.id), // 1337 replace: serverDoc.document // 1338 }); // 1339 // Call all flush callbacks. // 1340 _.each(serverDoc.flushCallbacks, function (c) { // 1341 c(); // 1342 }); // 1343 // 1344 // Delete this completed serverDocument. Don't bother to GC empty // 1345 // IdMaps inside self._serverDocuments, since there probably aren't // 1346 // many collections and they'll be written repeatedly. // 1347 self._serverDocuments[written.collection].remove(written.id); // 1348 } // 1349 }); // 1350 delete self._documentsWrittenByStub[methodId]; // 1351 // 1352 // We want to call the data-written callback, but we can't do so until all // 1353 // currently buffered messages are flushed. // 1354 var callbackInvoker = self._methodInvokers[methodId]; // 1355 if (!callbackInvoker) // 1356 throw new Error("No callback invoker for method " + methodId); // 1357 self._runWhenAllServerDocsAreFlushed( // 1358 _.bind(callbackInvoker.dataVisible, callbackInvoker)); // 1359 }); // 1360 }, // 1361 // 1362 _process_ready: function (msg, updates) { // 1363 var self = this; // 1364 // Process "sub ready" messages. "sub ready" messages don't take effect // 1365 // until all current server documents have been flushed to the local // 1366 // database. We can use a write fence to implement this. // 1367 _.each(msg.subs, function (subId) { // 1368 self._runWhenAllServerDocsAreFlushed(function () { // 1369 var subRecord = self._subscriptions[subId]; // 1370 // Did we already unsubscribe? // 1371 if (!subRecord) // 1372 return; // 1373 // Did we already receive a ready message? (Oops!) // 1374 if (subRecord.ready) // 1375 return; // 1376 subRecord.readyCallback && subRecord.readyCallback(); // 1377 subRecord.ready = true; // 1378 subRecord.readyDeps.changed(); // 1379 }); // 1380 }); // 1381 }, // 1382 // 1383 // Ensures that "f" will be called after all documents currently in // 1384 // _serverDocuments have been written to the local cache. f will not be called // 1385 // if the connection is lost before then! // 1386 _runWhenAllServerDocsAreFlushed: function (f) { // 1387 var self = this; // 1388 var runFAfterUpdates = function () { // 1389 self._afterUpdateCallbacks.push(f); // 1390 }; // 1391 var unflushedServerDocCount = 0; // 1392 var onServerDocFlush = function () { // 1393 --unflushedServerDocCount; // 1394 if (unflushedServerDocCount === 0) { // 1395 // This was the last doc to flush! Arrange to run f after the updates // 1396 // have been applied. // 1397 runFAfterUpdates(); // 1398 } // 1399 }; // 1400 _.each(self._serverDocuments, function (collectionDocs) { // 1401 collectionDocs.forEach(function (serverDoc) { // 1402 var writtenByStubForAMethodWithSentMessage = _.any( // 1403 serverDoc.writtenByStubs, function (dummy, methodId) { // 1404 var invoker = self._methodInvokers[methodId]; // 1405 return invoker && invoker.sentMessage; // 1406 }); // 1407 if (writtenByStubForAMethodWithSentMessage) { // 1408 ++unflushedServerDocCount; // 1409 serverDoc.flushCallbacks.push(onServerDocFlush); // 1410 } // 1411 }); // 1412 }); // 1413 if (unflushedServerDocCount === 0) { // 1414 // There aren't any buffered docs --- we can call f as soon as the current // 1415 // round of updates is applied! // 1416 runFAfterUpdates(); // 1417 } // 1418 }, // 1419 // 1420 _livedata_nosub: function (msg) { // 1421 var self = this; // 1422 // 1423 // First pass it through _livedata_data, which only uses it to help get // 1424 // towards quiescence. // 1425 self._livedata_data(msg); // 1426 // 1427 // Do the rest of our processing immediately, with no // 1428 // buffering-until-quiescence. // 1429 // 1430 // we weren't subbed anyway, or we initiated the unsub. // 1431 if (!_.has(self._subscriptions, msg.id)) // 1432 return; // 1433 // 1434 // XXX COMPAT WITH 1.0.3.1 #errorCallback // 1435 var errorCallback = self._subscriptions[msg.id].errorCallback; // 1436 var stopCallback = self._subscriptions[msg.id].stopCallback; // 1437 // 1438 self._subscriptions[msg.id].remove(); // 1439 // 1440 var meteorErrorFromMsg = function (msgArg) { // 1441 return msgArg && msgArg.error && new Meteor.Error( // 1442 msgArg.error.error, msgArg.error.reason, msgArg.error.details); // 1443 } // 1444 // 1445 // XXX COMPAT WITH 1.0.3.1 #errorCallback // 1446 if (errorCallback && msg.error) { // 1447 errorCallback(meteorErrorFromMsg(msg)); // 1448 } // 1449 // 1450 if (stopCallback) { // 1451 stopCallback(meteorErrorFromMsg(msg)); // 1452 } // 1453 }, // 1454 // 1455 _process_nosub: function () { // 1456 // This is called as part of the "buffer until quiescence" process, but // 1457 // nosub's effect is always immediate. It only goes in the buffer at all // 1458 // because it's possible for a nosub to be the thing that triggers // 1459 // quiescence, if we were waiting for a sub to be revived and it dies // 1460 // instead. // 1461 }, // 1462 // 1463 _livedata_result: function (msg) { // 1464 // id, result or error. error has error (code), reason, details // 1465 // 1466 var self = this; // 1467 // 1468 // find the outstanding request // 1469 // should be O(1) in nearly all realistic use cases // 1470 if (_.isEmpty(self._outstandingMethodBlocks)) { // 1471 Meteor._debug("Received method result but no methods outstanding"); // 1472 return; // 1473 } // 1474 var currentMethodBlock = self._outstandingMethodBlocks[0].methods; // 1475 var m; // 1476 for (var i = 0; i < currentMethodBlock.length; i++) { // 1477 m = currentMethodBlock[i]; // 1478 if (m.methodId === msg.id) // 1479 break; // 1480 } // 1481 // 1482 if (!m) { // 1483 Meteor._debug("Can't match method response to original method call", msg); // 1484 return; // 1485 } // 1486 // 1487 // Remove from current method block. This may leave the block empty, but we // 1488 // don't move on to the next block until the callback has been delivered, in // 1489 // _outstandingMethodFinished. // 1490 currentMethodBlock.splice(i, 1); // 1491 // 1492 if (_.has(msg, 'error')) { // 1493 m.receiveResult(new Meteor.Error( // 1494 msg.error.error, msg.error.reason, // 1495 msg.error.details)); // 1496 } else { // 1497 // msg.result may be undefined if the method didn't return a // 1498 // value // 1499 m.receiveResult(undefined, msg.result); // 1500 } // 1501 }, // 1502 // 1503 // Called by MethodInvoker after a method's callback is invoked. If this was // 1504 // the last outstanding method in the current block, runs the next block. If // 1505 // there are no more methods, consider accepting a hot code push. // 1506 _outstandingMethodFinished: function () { // 1507 var self = this; // 1508 if (self._anyMethodsAreOutstanding()) // 1509 return; // 1510 // 1511 // No methods are outstanding. This should mean that the first block of // 1512 // methods is empty. (Or it might not exist, if this was a method that // 1513 // half-finished before disconnect/reconnect.) // 1514 if (! _.isEmpty(self._outstandingMethodBlocks)) { // 1515 var firstBlock = self._outstandingMethodBlocks.shift(); // 1516 if (! _.isEmpty(firstBlock.methods)) // 1517 throw new Error("No methods outstanding but nonempty block: " + // 1518 JSON.stringify(firstBlock)); // 1519 // 1520 // Send the outstanding methods now in the first block. // 1521 if (!_.isEmpty(self._outstandingMethodBlocks)) // 1522 self._sendOutstandingMethods(); // 1523 } // 1524 // 1525 // Maybe accept a hot code push. // 1526 self._maybeMigrate(); // 1527 }, // 1528 // 1529 // Sends messages for all the methods in the first block in // 1530 // _outstandingMethodBlocks. // 1531 _sendOutstandingMethods: function() { // 1532 var self = this; // 1533 if (_.isEmpty(self._outstandingMethodBlocks)) // 1534 return; // 1535 _.each(self._outstandingMethodBlocks[0].methods, function (m) { // 1536 m.sendMessage(); // 1537 }); // 1538 }, // 1539 // 1540 _livedata_error: function (msg) { // 1541 Meteor._debug("Received error from server: ", msg.reason); // 1542 if (msg.offendingMessage) // 1543 Meteor._debug("For: ", msg.offendingMessage); // 1544 }, // 1545 // 1546 _callOnReconnectAndSendAppropriateOutstandingMethods: function() { // 1547 var self = this; // 1548 var oldOutstandingMethodBlocks = self._outstandingMethodBlocks; // 1549 self._outstandingMethodBlocks = []; // 1550 // 1551 self.onReconnect(); // 1552 // 1553 if (_.isEmpty(oldOutstandingMethodBlocks)) // 1554 return; // 1555 // 1556 // We have at least one block worth of old outstanding methods to try // 1557 // again. First: did onReconnect actually send anything? If not, we just // 1558 // restore all outstanding methods and run the first block. // 1559 if (_.isEmpty(self._outstandingMethodBlocks)) { // 1560 self._outstandingMethodBlocks = oldOutstandingMethodBlocks; // 1561 self._sendOutstandingMethods(); // 1562 return; // 1563 } // 1564 // 1565 // OK, there are blocks on both sides. Special case: merge the last block of // 1566 // the reconnect methods with the first block of the original methods, if // 1567 // neither of them are "wait" blocks. // 1568 if (!_.last(self._outstandingMethodBlocks).wait && // 1569 !oldOutstandingMethodBlocks[0].wait) { // 1570 _.each(oldOutstandingMethodBlocks[0].methods, function (m) { // 1571 _.last(self._outstandingMethodBlocks).methods.push(m); // 1572 // 1573 // If this "last block" is also the first block, send the message. // 1574 if (self._outstandingMethodBlocks.length === 1) // 1575 m.sendMessage(); // 1576 }); // 1577 // 1578 oldOutstandingMethodBlocks.shift(); // 1579 } // 1580 // 1581 // Now add the rest of the original blocks on. // 1582 _.each(oldOutstandingMethodBlocks, function (block) { // 1583 self._outstandingMethodBlocks.push(block); // 1584 }); // 1585 }, // 1586 // 1587 // We can accept a hot code push if there are no methods in flight. // 1588 _readyToMigrate: function() { // 1589 var self = this; // 1590 return _.isEmpty(self._methodInvokers); // 1591 }, // 1592 // 1593 // If we were blocking a migration, see if it's now possible to continue. // 1594 // Call whenever the set of outstanding/blocked methods shrinks. // 1595 _maybeMigrate: function () { // 1596 var self = this; // 1597 if (self._retryMigrate && self._readyToMigrate()) { // 1598 self._retryMigrate(); // 1599 self._retryMigrate = null; // 1600 } // 1601 } // 1602 }); // 1603 // 1604 LivedataTest.Connection = Connection; // 1605 // 1606 // @param url {String} URL to Meteor app, // 1607 // e.g.: // 1608 // "subdomain.meteor.com", // 1609 // "http://subdomain.meteor.com", // 1610 // "/", // 1611 // "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" // 1612 // 1613 /** // 1614 * @summary Connect to the server of a different Meteor application to subscribe to its document sets and invoke its remote methods. * @locus Anywhere // 1616 * @param {String} url The URL of another Meteor application. // 1617 */ // 1618 DDP.connect = function (url, options) { // 1619 var ret = new Connection(url, options); // 1620 allConnections.push(ret); // hack. see below. // 1621 return ret; // 1622 }; // 1623 // 1624 // Hack for `spiderable` package: a way to see if the page is done // 1625 // loading all the data it needs. // 1626 // // 1627 allConnections = []; // 1628 DDP._allSubscriptionsReady = function () { // 1629 return _.all(allConnections, function (conn) { // 1630 return _.all(conn._subscriptions, function (sub) { // 1631 return sub.ready; // 1632 }); // 1633 }); // 1634 }; // 1635 // 1636 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/ddp/client_convenience.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Meteor.refresh can be called on the client (if you're in common code) but it // 1 // only has an effect on the server. // 2 Meteor.refresh = function (notification) { // 3 }; // 4 // 5 if (Meteor.isClient) { // 6 // By default, try to connect back to the same endpoint as the page // 7 // was served from. // 8 // // 9 // XXX We should be doing this a different way. Right now we don't // 10 // include ROOT_URL_PATH_PREFIX when computing ddpUrl. (We don't // 11 // include it on the server when computing // 12 // DDP_DEFAULT_CONNECTION_URL, and we don't include it in our // 13 // default, '/'.) We get by with this because DDP.connect then // 14 // forces the URL passed to it to be interpreted relative to the // 15 // app's deploy path, even if it is absolute. Instead, we should // 16 // make DDP_DEFAULT_CONNECTION_URL, if set, include the path prefix; // 17 // make the default ddpUrl be '' rather that '/'; and make // 18 // _translateUrl in stream_client_common.js not force absolute paths // 19 // to be treated like relative paths. See also // 20 // stream_client_common.js #RationalizingRelativeDDPURLs // 21 var ddpUrl = '/'; // 22 if (typeof __meteor_runtime_config__ !== "undefined") { // 23 if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL) // 24 ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL; // 25 } // 26 // 27 var retry = new Retry(); // 28 // 29 var onDDPVersionNegotiationFailure = function (description) { // 30 Meteor._debug(description); // 31 if (Package.reload) { // 32 var migrationData = Package.reload.Reload._migrationData('livedata') || {}; // 33 var failures = migrationData.DDPVersionNegotiationFailures || 0; // 34 ++failures; // 35 Package.reload.Reload._onMigrate('livedata', function () { // 36 return [true, {DDPVersionNegotiationFailures: failures}]; // 37 }); // 38 retry.retryLater(failures, function () { // 39 Package.reload.Reload._reload(); // 40 }); // 41 } // 42 }; // 43 // 44 Meteor.connection = // 45 DDP.connect(ddpUrl, { // 46 onDDPVersionNegotiationFailure: onDDPVersionNegotiationFailure // 47 }); // 48 // 49 // Proxy the public methods of Meteor.connection so they can // 50 // be called directly on Meteor. // 51 _.each(['subscribe', 'methods', 'call', 'apply', 'status', 'reconnect', // 52 'disconnect'], // 53 function (name) { // 54 Meteor[name] = _.bind(Meteor.connection[name], Meteor.connection); // 55 }); // 56 } else { // 57 // Never set up a default connection on the server. Don't even map // 58 // subscribe/call/etc onto Meteor. // 59 Meteor.connection = null; // 60 } // 61 // 62 // Meteor.connection used to be called // 63 // Meteor.default_connection. Provide backcompat as a courtesy even // 64 // though it was never documented. // 65 // XXX COMPAT WITH 0.6.4 // 66 Meteor.default_connection = Meteor.connection; // 67 // 68 // We should transition from Meteor.connect to DDP.connect. // 69 // XXX COMPAT WITH 0.6.4 // 70 Meteor.connect = DDP.connect; // 71 // 72 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); /* Exports */ if (typeof Package === 'undefined') Package = {}; Package.ddp = { DDP: DDP, LivedataTest: LivedataTest }; })();