├── .github ├── FUNDING.yml └── workflows │ └── maven.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── modules └── javascript │ ├── package.json │ ├── pom.xml │ └── src │ └── main │ └── webapp │ ├── WEB-INF │ └── web.xml │ └── javascript │ └── atmosphere.js └── pom.xml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [atmosphere] 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up JDK 1.8 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.8 16 | - name: Build with Maven 17 | run: mvn -B package --file pom.xml 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *~ 3 | .*.swp 4 | .*.swo 5 | .loadpath 6 | .buildpath 7 | .project 8 | .settings 9 | .classpath 10 | .metadata 11 | .idea 12 | *.iml 13 | *.ipr 14 | *.iws 15 | nbproject 16 | .DS_Store 17 | target 18 | test-output 19 | nbactions.xml 20 | samples/gwt-demo/src/main/webapp/WEB-INF/classes/ 21 | samples/gwt-chat/src/main/webapp/WEB-INF/classes/ 22 | samples/gwt-conn-share/src/main/webapp/WEB-INF/classes/ 23 | atlassian-ide-plugin.xml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | language: javascript 4 | jdk: 5 | - openjdk8 6 | before_install: 'mvn -version' 7 | install: 'mvn clean install' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Welcome to Atmosphere: The Asynchronous WebSocket/Comet Framework 2 | Atmosphere transparently supports WebSockets, Server Sent Events (SSE), Long-Polling, HTTP Streaming (Forever frame) and JSONP. 3 | 4 | * [npm/Node.js client](https://github.com/Atmosphere/atmosphere.js-node) 5 | 6 | ## Install 7 | 8 | * NPM - [atmosphere.js](https://www.npmjs.com/package/atmosphere.js) 9 | 10 | ### maven 11 | 12 | ```xml 13 | 14 | org.atmosphere.client 15 | javascript 16 | 4.0.0 17 | 18 | ``` 19 | 20 | ### npm 21 | 22 | ```shell 23 | npm install atmosphere.js 24 | ``` 25 | 26 | Best way to start with Atmosphere is to use chatGPT and ask for an example! 27 | -------------------------------------------------------------------------------- /modules/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atmosphere.js", 3 | "version": "3.0.3", 4 | "description": "", 5 | "main": "src/main/webapp/javascript/atmosphere.js", 6 | "registry": "npm", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Atmosphere/atmosphere-javascript.git" 13 | }, 14 | "author": "Jeanfrancois Arcand", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/Atmosphere/atmosphere-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/Atmosphere/atmosphere-javascript#readme" 20 | } 21 | -------------------------------------------------------------------------------- /modules/javascript/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.atmosphere 5 | javascript-project 6 | 4.0.2-SNAPSHOT 7 | ../../pom.xml 8 | 9 | 4.0.0 10 | org.atmosphere.client 11 | javascript 12 | war 13 | 4.0.2-SNAPSHOT 14 | javascript 15 | 16 | 17 | 18 | 19 | 20 | net.alchim31.maven 21 | yuicompressor-maven-plugin 22 | 1.5.1 23 | 24 | 25 | 26 | compress 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /modules/javascript/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | DUMMY web.xml 2 | -------------------------------------------------------------------------------- /modules/javascript/src/main/webapp/javascript/atmosphere.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2025 Async-IO.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Atmosphere.js 18 | * https://github.com/Atmosphere/atmosphere-javascript 19 | * 20 | * API reference 21 | * https://github.com/Atmosphere/atmosphere/wiki/jQuery.atmosphere.js-API 22 | * 23 | * Highly inspired by 24 | * - Portal by Donghwan Kim http://flowersinthesand.github.io/portal/ 25 | */ 26 | (function (root, factory) { 27 | if (typeof define === "function" && define.amd) { 28 | // AMD 29 | define(factory); 30 | } else if (typeof exports !== 'undefined') { 31 | // CommonJS 32 | module.exports = factory(); 33 | } else { 34 | // Browser globals, Window 35 | root.atmosphere = factory(); 36 | } 37 | }(this, function () { 38 | 39 | "use strict"; 40 | 41 | var offline = false, 42 | requests = [], 43 | callbacks = [], 44 | uuid = 0; 45 | 46 | /** 47 | * {boolean} If window beforeUnload event has been called. 48 | * Flag will be reset after 5000 ms 49 | * 50 | * @private 51 | */ 52 | var _beforeUnloadState = false; 53 | 54 | var atmosphere = { 55 | version: "4.0.2", 56 | onError: function (response) { 57 | }, 58 | onClose: function (response) { 59 | }, 60 | onOpen: function (response) { 61 | }, 62 | onReopen: function (response) { 63 | }, 64 | onMessage: function (response) { 65 | }, 66 | onReconnect: function (request, response) { 67 | }, 68 | onMessagePublished: function (response) { 69 | }, 70 | onTransportFailure: function (errorMessage, _request) { 71 | }, 72 | onLocalMessage: function (response) { 73 | }, 74 | onFailureToReconnect: function (request, response) { 75 | }, 76 | onClientTimeout: function (request) { 77 | }, 78 | onOpenAfterResume: function (request) { 79 | }, 80 | 81 | /** 82 | * Creates an object based on an atmosphere subscription that exposes functions defined by the Websocket interface. 83 | * 84 | * @class WebsocketApiAdapter 85 | * @param {Object} request the request object to build the underlying subscription 86 | * @constructor 87 | */ 88 | WebsocketApiAdapter: function (request) { 89 | var _socket, _adapter; 90 | 91 | /** 92 | * Overrides the onMessage callback in given request. 93 | * 94 | * @method onMessage 95 | * @param {Object} e the event object 96 | */ 97 | request.onMessage = function (e) { 98 | _adapter.onmessage({ data: e.responseBody }); 99 | }; 100 | 101 | /** 102 | * Overrides the onMessagePublished callback in given request. 103 | * 104 | * @method onMessagePublished 105 | * @param {Object} e the event object 106 | */ 107 | request.onMessagePublished = function (e) { 108 | _adapter.onmessage({ data: e.responseBody }); 109 | }; 110 | 111 | /** 112 | * Overrides the onOpen callback in given request to proxy the event to the adapter. 113 | * 114 | * @method onOpen 115 | * @param {Object} e the event object 116 | */ 117 | request.onOpen = function (e) { 118 | _adapter.onopen(e); 119 | }; 120 | 121 | _adapter = { 122 | close: function () { 123 | _socket.close(); 124 | }, 125 | 126 | send: function (data) { 127 | _socket.push(data); 128 | }, 129 | 130 | onmessage: function (e) { 131 | }, 132 | 133 | onopen: function (e) { 134 | }, 135 | 136 | onclose: function (e) { 137 | }, 138 | 139 | onerror: function (e) { 140 | 141 | } 142 | }; 143 | _socket = new atmosphere.subscribe(request); 144 | 145 | return _adapter; 146 | }, 147 | 148 | AtmosphereRequest: function (options) { 149 | 150 | /** 151 | * {Object} Request parameters. 152 | * 153 | * @private 154 | */ 155 | var _request = { 156 | timeout: 300000, 157 | method: 'GET', 158 | headers: {}, 159 | contentType: '', 160 | callback: null, 161 | url: '', 162 | data: '', 163 | suspend: true, 164 | maxRequest: -1, 165 | reconnect: true, 166 | mrequest: undefined, 167 | maxStreamingLength: 10000000, 168 | lastIndex: 0, 169 | logLevel: 'info', 170 | requestCount: 0, 171 | fallbackMethod: 'GET', 172 | fallbackTransport: 'streaming', 173 | transport: 'long-polling', 174 | webSocketUrl: undefined, 175 | webSocketImpl: null, 176 | webSocketBinaryType: null, 177 | dispatchUrl: null, 178 | webSocketPathDelimiter: "@@", 179 | enableXDR: false, 180 | rewriteURL: false, 181 | attachHeadersAsQueryString: true, 182 | executeCallbackBeforeReconnect: false, 183 | readyState: 0, 184 | withCredentials: false, 185 | trackMessageLength: false, 186 | messageDelimiter: '|', 187 | connectTimeout: -1, 188 | reconnectInterval: 0, 189 | dropHeaders: true, 190 | uuid: 0, 191 | shared: false, 192 | readResponsesHeaders: false, 193 | maxReconnectOnClose: 5, 194 | enableProtocol: true, 195 | disableDisconnect: false, 196 | pollingInterval: 0, 197 | heartbeat: { 198 | client: null, 199 | server: null 200 | }, 201 | ackInterval: 0, 202 | reconnectOnServerError: true, 203 | handleOnlineOffline: true, 204 | maxWebsocketErrorRetries: 1, 205 | curWebsocketErrorRetries: 0, 206 | unloadBackwardCompat: !navigator.sendBeacon, 207 | useBeforeUnloadForCleanup: true, 208 | id: undefined, 209 | openId: undefined, 210 | reconnectId: undefined, 211 | firstMessage: undefined, 212 | isOpen: undefined, 213 | isReopen: undefined, 214 | closed: undefined, 215 | ctime: undefined, 216 | heartbeatTimer: undefined, 217 | force: undefined, 218 | onError: function (response) { 219 | }, 220 | onClose: function (response) { 221 | }, 222 | onOpen: function (response) { 223 | }, 224 | onMessage: function (response) { 225 | }, 226 | onReopen: function (request, response) { 227 | }, 228 | onReconnect: function (request, response) { 229 | }, 230 | onMessagePublished: function (response) { 231 | }, 232 | onTransportFailure: function (reason, request) { 233 | }, 234 | onLocalMessage: function (request) { 235 | }, 236 | onFailureToReconnect: function (request, response) { 237 | }, 238 | onClientTimeout: function (request) { 239 | }, 240 | onOpenAfterResume: function (request) { 241 | } 242 | }; 243 | 244 | /** 245 | * {Object} Request's last response. 246 | * 247 | * @private 248 | */ 249 | var _response = { 250 | status: 200, 251 | reasonPhrase: "OK", 252 | responseBody: '', 253 | messages: [], 254 | headers: {}, 255 | state: "messageReceived", 256 | transport: "polling", 257 | error: null, 258 | request: null, 259 | partialMessage: "", 260 | errorHandled: false, 261 | closedByClientTimeout: false, 262 | ffTryingReconnect: false 263 | }; 264 | 265 | /** 266 | * {websocket} Opened web socket. 267 | * 268 | * @private 269 | */ 270 | var _websocket = null; 271 | 272 | /** 273 | * {SSE} Opened SSE. 274 | * 275 | * @private 276 | */ 277 | var _sse = null; 278 | 279 | /** 280 | * {XMLHttpRequest, ActiveXObject} Opened ajax request (in case of http-streaming or long-polling) 281 | * 282 | * @private 283 | */ 284 | var _activeRequest = null; 285 | 286 | /** 287 | * {Object} Object use for streaming with IE. 288 | * 289 | * @private 290 | */ 291 | var _ieStream = null; 292 | 293 | /** 294 | * {Object} Object use for jsonp transport. 295 | * 296 | * @private 297 | */ 298 | var _jqxhr = null; 299 | 300 | /** 301 | * {boolean} If request has been subscribed or not. 302 | * 303 | * @private 304 | */ 305 | var _subscribed = true; 306 | 307 | /** 308 | * {number} Number of test reconnection. 309 | * 310 | * @private 311 | */ 312 | var _requestCount = 0; 313 | 314 | /** 315 | * The Heartbeat interval send by the server. 316 | * @type {int} 317 | * @private 318 | */ 319 | var _heartbeatInterval = 0; 320 | 321 | /** 322 | * The Heartbeat bytes send by the server. 323 | * @type {string} 324 | * @private 325 | */ 326 | var _heartbeatPadding = 'X'; 327 | 328 | /** 329 | * {boolean} If request is currently aborted. 330 | * 331 | * @private 332 | */ 333 | var _abortingConnection = false; 334 | 335 | /** 336 | * A local "channel' of communication. 337 | * 338 | * @private 339 | */ 340 | var _localSocketF = null; 341 | 342 | /** 343 | * The storage used. 344 | * 345 | * @private 346 | */ 347 | var _storageService; 348 | 349 | /** 350 | * Local communication 351 | * 352 | * @private 353 | */ 354 | var _localStorageService = null; 355 | 356 | /** 357 | * A Unique ID 358 | * 359 | * @private 360 | */ 361 | var guid = atmosphere.util.now(); 362 | 363 | /** Trace time */ 364 | var _traceTimer; 365 | 366 | /** Key for connection sharing */ 367 | var _sharingKey; 368 | 369 | /** 370 | * {number} Holds the timeout ID for the beforeUnload flag reset. 371 | * 372 | * @private 373 | */ 374 | var _beforeUnloadTimeoutId; 375 | 376 | // Automatic call to subscribe 377 | _subscribe(options); 378 | 379 | /** 380 | * Initialize atmosphere request object. 381 | * 382 | * @private 383 | */ 384 | function _init() { 385 | _subscribed = true; 386 | _abortingConnection = false; 387 | _requestCount = 0; 388 | 389 | _websocket = null; 390 | _sse = null; 391 | _activeRequest = null; 392 | _ieStream = null; 393 | } 394 | 395 | /** 396 | * Re-initialize atmosphere object. 397 | * 398 | * @private 399 | */ 400 | function _reinit() { 401 | _clearState(); 402 | _init(); 403 | } 404 | 405 | /** 406 | * Returns true if the given level is equal or above the configured log level. 407 | * 408 | * @private 409 | */ 410 | function _canLog(level) { 411 | if (level == 'debug') { 412 | return _request.logLevel === 'debug'; 413 | } else if (level == 'info') { 414 | return _request.logLevel === 'info' || _request.logLevel === 'debug'; 415 | } else if (level == 'warn') { 416 | return _request.logLevel === 'warn' || _request.logLevel === 'info' || _request.logLevel === 'debug'; 417 | } else if (level == 'error') { 418 | return _request.logLevel === 'error' || _request.logLevel === 'warn' || _request.logLevel === 'info' || _request.logLevel === 'debug'; 419 | } else { 420 | return false; 421 | } 422 | } 423 | 424 | function _debug(msg) { 425 | if (_canLog('debug')) { 426 | atmosphere.util.debug(new Date() + " Atmosphere: " + msg); 427 | } 428 | } 429 | 430 | /** 431 | * 432 | * @private 433 | */ 434 | function _verifyStreamingLength(ajaxRequest, rq) { 435 | // Wait to be sure we have the full message before closing. 436 | if (_response.partialMessage === "" && (rq.transport === 'streaming') && (ajaxRequest.responseText.length > rq.maxStreamingLength)) { 437 | return true; 438 | } 439 | return false; 440 | } 441 | 442 | /** 443 | * Disconnect 444 | * 445 | * @private 446 | */ 447 | function _disconnect() { 448 | if (_request.enableProtocol && !_request.disableDisconnect && !_request.firstMessage) { 449 | var query = "X-Atmosphere-Transport=close&X-Atmosphere-tracking-id=" + _request.uuid; 450 | 451 | atmosphere.util.each(_request.headers, function (name, value) { 452 | var h = atmosphere.util.isFunction(value) ? value.call(this, _request, _request, _response) : value; 453 | if (h != null) { 454 | query += "&" + encodeURIComponent(name) + "=" + encodeURIComponent(h); 455 | } 456 | }); 457 | 458 | var url = _request.url.replace(/([?&])_=[^&]*/, query); 459 | url = url + (url === _request.url ? (/\?/.test(_request.url) ? "&" : "?") + query : ""); 460 | 461 | var rq = { 462 | connected: false 463 | }; 464 | var closeR = new atmosphere.AtmosphereRequest(rq); 465 | closeR.connectTimeout = _request.connectTimeout; 466 | closeR.attachHeadersAsQueryString = false; 467 | closeR.dropHeaders = true; 468 | closeR.url = url; 469 | closeR.contentType = "text/plain"; 470 | closeR.transport = 'polling'; 471 | closeR.method = 'GET'; 472 | closeR.data = ''; 473 | closeR.heartbeat = null; 474 | if (_request.enableXDR) { 475 | closeR.enableXDR = _request.enableXDR 476 | } 477 | _pushOnClose("", closeR); 478 | } 479 | } 480 | 481 | /** 482 | * Close request. 483 | * 484 | * @private 485 | */ 486 | function _close() { 487 | _debug("Closing (AtmosphereRequest._close() called)"); 488 | 489 | _abortingConnection = true; 490 | if (_request.reconnectId) { 491 | clearTimeout(_request.reconnectId); 492 | delete _request.reconnectId; 493 | } 494 | 495 | if (_request.heartbeatTimer) { 496 | clearTimeout(_request.heartbeatTimer); 497 | } 498 | 499 | _request.reconnect = false; 500 | _response.request = _request; 501 | _response.state = 'unsubscribe'; 502 | _response.responseBody = ""; 503 | _response.status = 408; 504 | _response.partialMessage = ""; 505 | _request.curWebsocketErrorRetries = 0; 506 | _invokeCallback(); 507 | _disconnect(); 508 | _clearState(); 509 | } 510 | 511 | function _clearState() { 512 | _response.partialMessage = ""; 513 | if (_request.id) { 514 | clearTimeout(_request.id); 515 | } 516 | 517 | if (_request.heartbeatTimer) { 518 | clearTimeout(_request.heartbeatTimer); 519 | } 520 | 521 | // https://github.com/Atmosphere/atmosphere/issues/1860#issuecomment-74707226 522 | if (_request.reconnectId) { 523 | clearTimeout(_request.reconnectId); 524 | delete _request.reconnectId; 525 | } 526 | 527 | if (_ieStream != null) { 528 | _ieStream.close(); 529 | _ieStream = null; 530 | } 531 | if (_jqxhr != null) { 532 | _jqxhr.abort(); 533 | _jqxhr = null; 534 | } 535 | if (_activeRequest != null) { 536 | _activeRequest.abort(); 537 | _activeRequest = null; 538 | } 539 | if (_websocket != null) { 540 | if (_websocket.canSendMessage) { 541 | _debug("invoking .close() on WebSocket object"); 542 | _websocket.close(); 543 | } 544 | _websocket = null; 545 | } 546 | if (_sse != null) { 547 | _sse.close(); 548 | _sse = null; 549 | } 550 | _clearStorage(); 551 | } 552 | 553 | function _clearStorage() { 554 | // Stop sharing a connection 555 | if (_storageService != null) { 556 | // Clears trace timer 557 | clearInterval(_traceTimer); 558 | // Removes the trace 559 | document.cookie = _sharingKey + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; 560 | // The heir is the parent unless unloading 561 | _storageService.signal("close", { 562 | reason: "", 563 | heir: !_abortingConnection ? guid : (_storageService.get("children") || [])[0] 564 | }); 565 | _storageService.close(); 566 | } 567 | if (_localStorageService != null) { 568 | _localStorageService.close(); 569 | } 570 | } 571 | 572 | /** 573 | * Subscribe request using request transport.
574 | * If request is currently opened, this one will be closed. 575 | * 576 | * @param {Object} Request parameters. 577 | * @private 578 | */ 579 | function _subscribe(options) { 580 | _reinit(); 581 | 582 | _request = atmosphere.util.extend(_request, options); 583 | // Allow at least 1 request 584 | _request.mrequest = _request.reconnect; 585 | if (!_request.reconnect) { 586 | _request.reconnect = true; 587 | } 588 | } 589 | 590 | /** 591 | * Check if web socket is supported (check for custom implementation provided by request object or browser implementation). 592 | * 593 | * @returns {boolean} True if web socket is supported, false otherwise. 594 | * @private 595 | */ 596 | function _supportWebsocket() { 597 | return _request.webSocketImpl != null || window.WebSocket || window.MozWebSocket; 598 | } 599 | 600 | /** 601 | * Check if server side events (SSE) is supported (check for custom implementation provided by request object or browser implementation). 602 | * 603 | * @returns {boolean} True if web socket is supported, false otherwise. 604 | * @private 605 | */ 606 | function _supportSSE() { 607 | // Origin parts 608 | var url = atmosphere.util.getAbsoluteURL(_request.url.toLowerCase()); 609 | var parts = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/.exec(url); 610 | var crossOrigin = !!(parts && ( 611 | // protocol 612 | parts[1] != window.location.protocol || 613 | // hostname 614 | parts[2] != window.location.hostname || 615 | // port 616 | (parts[3] || (parts[1] === "http:" ? 80 : 443)) != (window.location.port || (window.location.protocol === "http:" ? 80 : 443)) 617 | )); 618 | return window.EventSource && (!crossOrigin || !atmosphere.util.browser.safari || atmosphere.util.browser.vmajor >= 7); 619 | } 620 | 621 | /** 622 | * Open request using request transport.
623 | * If request transport is 'websocket' but websocket can't be opened, request will automatically reconnect using fallback transport. 624 | * 625 | * @private 626 | */ 627 | function _execute() { 628 | // Shared across multiple tabs/windows. 629 | if (_request.shared) { 630 | _localStorageService = _local(_request); 631 | if (_localStorageService != null) { 632 | if (_canLog('debug')) { 633 | atmosphere.util.debug("Storage service available. All communication will be local"); 634 | } 635 | 636 | if (_localStorageService.open(_request)) { 637 | // Local connection. 638 | return; 639 | } 640 | } 641 | 642 | if (_canLog('debug')) { 643 | atmosphere.util.debug("No Storage service available."); 644 | } 645 | // Wasn't local or an error occurred 646 | _localStorageService = null; 647 | } 648 | 649 | // Protocol 650 | _request.firstMessage = uuid == 0 ? true : false; 651 | _request.isOpen = false; 652 | _request.ctime = atmosphere.util.now(); 653 | 654 | // We carry any UUID set by the user or from a previous connection. 655 | if (_request.uuid === 0) { 656 | _request.uuid = uuid; 657 | } 658 | _response.closedByClientTimeout = false; 659 | 660 | if (_request.transport === 'websocket') { 661 | if (!_supportWebsocket()) { 662 | _reconnectWithFallbackTransport("Websocket is not supported, using request.fallbackTransport (" + _request.fallbackTransport 663 | + ")"); 664 | } else { 665 | _executeWebSocket(false); 666 | } 667 | } else if (_request.transport === 'sse') { 668 | if (!_supportSSE()) { 669 | _reconnectWithFallbackTransport("Server Side Events(SSE) is not supported, using request.fallbackTransport (" 670 | + _request.fallbackTransport + ")"); 671 | } else { 672 | _executeSSE(false); 673 | } 674 | } else { 675 | _executeRequest(_request); 676 | } 677 | } 678 | 679 | function _local(request) { 680 | var trace, connector, orphan, name = "atmosphere-" + request.url, connectors = { 681 | storage: function () { 682 | function onstorage(event) { 683 | if (event.key === name && event.newValue) { 684 | listener(event.newValue); 685 | } 686 | } 687 | 688 | if (!atmosphere.util.storage) { 689 | return; 690 | } 691 | 692 | var storage = window.localStorage, 693 | get = function (key) { 694 | var item = storage.getItem(name + "-" + key); 695 | return item === null ? [] : JSON.parse(item); 696 | }, 697 | set = function (key, value) { 698 | storage.setItem(name + "-" + key, JSON.stringify(value)); 699 | }; 700 | 701 | return { 702 | init: function () { 703 | set("children", get("children").concat([guid])); 704 | atmosphere.util.on(window, "storage", onstorage); 705 | return get("opened"); 706 | }, 707 | signal: function (type, data) { 708 | storage.setItem(name, JSON.stringify({ 709 | target: "p", 710 | type: type, 711 | data: data 712 | })); 713 | }, 714 | close: function () { 715 | var children = get("children"); 716 | 717 | atmosphere.util.off(window, "storage", onstorage); 718 | if (children) { 719 | if (removeFromArray(children, request.id)) { 720 | set("children", children); 721 | } 722 | } 723 | } 724 | }; 725 | }, 726 | windowref: function () { 727 | var win = window.open("", name.replace(/\W/g, "")); 728 | 729 | if (!win || win.closed || !win.callbacks) { 730 | return; 731 | } 732 | 733 | return { 734 | init: function () { 735 | win.callbacks.push(listener); 736 | win.children.push(guid); 737 | return win.opened; 738 | }, 739 | signal: function (type, data) { 740 | if (!win.closed && win.fire) { 741 | win.fire(JSON.stringify({ 742 | target: "p", 743 | type: type, 744 | data: data 745 | })); 746 | } 747 | }, 748 | close: function () { 749 | // Removes traces only if the parent is alive 750 | if (!orphan) { 751 | removeFromArray(win.callbacks, listener); 752 | removeFromArray(win.children, guid); 753 | } 754 | } 755 | 756 | }; 757 | } 758 | }; 759 | 760 | function removeFromArray(array, val) { 761 | var i, length = array.length; 762 | 763 | for (i = 0; i < length; i++) { 764 | if (array[i] === val) { 765 | array.splice(i, 1); 766 | } 767 | } 768 | 769 | return length !== array.length; 770 | } 771 | 772 | // Receives open, close and message command from the parent 773 | function listener(string) { 774 | var command = JSON.parse(string), data = command.data; 775 | 776 | if (command.target === "c") { 777 | switch (command.type) { 778 | case "open": 779 | _open("opening", 'local', _request); 780 | break; 781 | case "close": 782 | if (!orphan) { 783 | orphan = true; 784 | if (data.reason === "aborted") { 785 | _close(); 786 | } else { 787 | // Gives the heir some time to reconnect 788 | if (data.heir === guid) { 789 | _execute(); 790 | } else { 791 | setTimeout(function () { 792 | _execute(); 793 | }, 100); 794 | } 795 | } 796 | } 797 | break; 798 | case "message": 799 | _prepareCallback(data, "messageReceived", 200, request.transport); 800 | break; 801 | case "localMessage": 802 | _localMessage(data); 803 | break; 804 | } 805 | } 806 | } 807 | 808 | function findTrace() { 809 | var matcher = new RegExp("(?:^|; )(" + encodeURIComponent(name) + ")=([^;]*)").exec(document.cookie); 810 | if (matcher) { 811 | return JSON.parse(decodeURIComponent(matcher[2])); 812 | } 813 | } 814 | 815 | // Finds and validates the parent socket's trace from the cookie 816 | trace = findTrace(); 817 | if (!trace || atmosphere.util.now() - trace.ts > 1000) { 818 | return; 819 | } 820 | 821 | // Chooses a connector 822 | connector = connectors.storage() || connectors.windowref(); 823 | if (!connector) { 824 | return; 825 | } 826 | 827 | return { 828 | open: function () { 829 | var parentOpened; 830 | 831 | // Checks the shared one is alive 832 | _traceTimer = setInterval(function () { 833 | var oldTrace = trace; 834 | trace = findTrace(); 835 | if (!trace || oldTrace.ts === trace.ts) { 836 | // Simulates a close signal 837 | listener(JSON.stringify({ 838 | target: "c", 839 | type: "close", 840 | data: { 841 | reason: "error", 842 | heir: oldTrace.heir 843 | } 844 | })); 845 | } 846 | }, 1000); 847 | 848 | parentOpened = connector.init(); 849 | if (parentOpened) { 850 | // Firing the open event without delay robs the user of the opportunity to bind connecting event handlers 851 | setTimeout(function () { 852 | _open("opening", 'local', request); 853 | }, 50); 854 | } 855 | return parentOpened; 856 | }, 857 | send: function (event) { 858 | connector.signal("send", event); 859 | }, 860 | localSend: function (event) { 861 | connector.signal("localSend", JSON.stringify({ 862 | id: guid, 863 | event: event 864 | })); 865 | }, 866 | close: function () { 867 | // Do not signal the parent if this method is executed by the unload event handler 868 | if (!_abortingConnection) { 869 | clearInterval(_traceTimer); 870 | connector.signal("close"); 871 | connector.close(); 872 | } 873 | } 874 | }; 875 | } 876 | 877 | function share() { 878 | var storageService, name = "atmosphere-" + _request.url, servers = { 879 | // Powered by the storage event and the localStorage 880 | // http://www.w3.org/TR/webstorage/#event-storage 881 | storage: function () { 882 | function onstorage(event) { 883 | // When a deletion, newValue initialized to null 884 | if (event.key === name && event.newValue) { 885 | listener(event.newValue); 886 | } 887 | } 888 | 889 | if (!atmosphere.util.storage) { 890 | return; 891 | } 892 | 893 | var storage = window.localStorage; 894 | 895 | return { 896 | init: function () { 897 | // Handles the storage event 898 | atmosphere.util.on(window, "storage", onstorage); 899 | }, 900 | signal: function (type, data) { 901 | storage.setItem(name, JSON.stringify({ 902 | target: "c", 903 | type: type, 904 | data: data 905 | })); 906 | }, 907 | get: function (key) { 908 | return JSON.parse(storage.getItem(name + "-" + key)); 909 | }, 910 | set: function (key, value) { 911 | storage.setItem(name + "-" + key, JSON.stringify(value)); 912 | }, 913 | close: function () { 914 | atmosphere.util.off(window, "storage", onstorage); 915 | storage.removeItem(name); 916 | storage.removeItem(name + "-opened"); 917 | storage.removeItem(name + "-children"); 918 | } 919 | 920 | }; 921 | }, 922 | // Powered by the window.open method 923 | // https://developer.mozilla.org/en/DOM/window.open 924 | windowref: function () { 925 | // Internet Explorer raises an invalid argument error 926 | // when calling the window.open method with the name containing non-word characters 927 | var neim = name.replace(/\W/g, ""), container = document.getElementById(neim), win; 928 | 929 | if (!container) { 930 | container = document.createElement("div"); 931 | container.id = neim; 932 | container.style.display = "none"; 933 | container.innerHTML = ''; 934 | document.body.appendChild(container); 935 | } 936 | 937 | win = container.firstChild.contentWindow; 938 | 939 | return { 940 | init: function () { 941 | // Callbacks from different windows 942 | win.callbacks = [listener]; 943 | // In IE 8 and less, only string argument can be safely passed to the function in other window 944 | win.fire = function (string) { 945 | var i; 946 | 947 | for (i = 0; i < win.callbacks.length; i++) { 948 | win.callbacks[i](string); 949 | } 950 | }; 951 | }, 952 | signal: function (type, data) { 953 | if (!win.closed && win.fire) { 954 | win.fire(JSON.stringify({ 955 | target: "c", 956 | type: type, 957 | data: data 958 | })); 959 | } 960 | }, 961 | get: function (key) { 962 | return !win.closed ? win[key] : null; 963 | }, 964 | set: function (key, value) { 965 | if (!win.closed) { 966 | win[key] = value; 967 | } 968 | }, 969 | close: function () { 970 | } 971 | }; 972 | } 973 | }; 974 | 975 | // Receives send and close command from the children 976 | function listener(string) { 977 | var command = JSON.parse(string), data = command.data; 978 | 979 | if (command.target === "p") { 980 | switch (command.type) { 981 | case "send": 982 | _push(data, _request); 983 | break; 984 | case "localSend": 985 | _localMessage(data); 986 | break; 987 | case "close": 988 | _close(); 989 | break; 990 | } 991 | } 992 | } 993 | 994 | _localSocketF = function propagateMessageEvent(context) { 995 | storageService.signal("message", context); 996 | }; 997 | 998 | function leaveTrace() { 999 | document.cookie = _sharingKey + "=" + 1000 | // Opera's JSON implementation ignores a number whose a last digit of 0 strangely 1001 | // but has no problem with a number whose a last digit of 9 + 1 1002 | encodeURIComponent(JSON.stringify({ 1003 | ts: atmosphere.util.now() + 1, 1004 | heir: (storageService.get("children") || [])[0] 1005 | })) + "; path=/"; 1006 | } 1007 | 1008 | // Chooses a storageService 1009 | storageService = servers.storage() || servers.windowref(); 1010 | storageService.init(); 1011 | 1012 | if (_canLog('debug')) { 1013 | atmosphere.util.debug("Installed StorageService " + storageService); 1014 | } 1015 | 1016 | // List of children sockets 1017 | storageService.set("children", []); 1018 | 1019 | if (storageService.get("opened") != null && !storageService.get("opened")) { 1020 | // Flag indicating the parent socket is opened 1021 | storageService.set("opened", false); 1022 | } 1023 | // Leaves traces 1024 | _sharingKey = encodeURIComponent(name); 1025 | leaveTrace(); 1026 | _traceTimer = setInterval(leaveTrace, 1000); 1027 | 1028 | _storageService = storageService; 1029 | } 1030 | 1031 | /** 1032 | * @private 1033 | */ 1034 | function _open(state, transport, request) { 1035 | if (_request.shared && transport !== 'local') { 1036 | share(); 1037 | } 1038 | 1039 | if (_storageService != null) { 1040 | _storageService.set("opened", true); 1041 | } 1042 | 1043 | request.close = function () { 1044 | _close(); 1045 | }; 1046 | 1047 | if (_requestCount > 0 && state === 're-connecting') { 1048 | request.isReopen = true; 1049 | _tryingToReconnect(_response); 1050 | } else if (!_response.error) { 1051 | _response.request = request; 1052 | var prevState = _response.state; 1053 | _response.state = state; 1054 | var prevTransport = _response.transport; 1055 | _response.transport = transport; 1056 | 1057 | var _body = _response.responseBody; 1058 | _invokeCallback(); 1059 | _response.responseBody = _body; 1060 | 1061 | _response.state = prevState; 1062 | _response.transport = prevTransport; 1063 | } 1064 | } 1065 | 1066 | /** 1067 | * Execute request using jsonp transport. 1068 | * 1069 | * @param request {Object} request Request parameters, if undefined _request object will be used. 1070 | * @private 1071 | */ 1072 | function _jsonp(request) { 1073 | // When CORS is enabled, make sure we force the proper transport. 1074 | request.transport = "jsonp"; 1075 | 1076 | var rq = _request, script; 1077 | if ((request != null) && (typeof (request) !== 'undefined')) { 1078 | rq = request; 1079 | } 1080 | 1081 | _jqxhr = { 1082 | open: function () { 1083 | var callback = "atmosphere" + (++guid); 1084 | 1085 | function _reconnectOnFailure() { 1086 | rq.lastIndex = 0; 1087 | 1088 | if (rq.openId) { 1089 | clearTimeout(rq.openId); 1090 | } 1091 | 1092 | if (rq.heartbeatTimer) { 1093 | clearTimeout(rq.heartbeatTimer); 1094 | } 1095 | 1096 | if (rq.reconnect && _requestCount++ < rq.maxReconnectOnClose) { 1097 | _open('re-connecting', rq.transport, rq); 1098 | _reconnect(_jqxhr, rq, request.reconnectInterval); 1099 | rq.openId = setTimeout(function () { 1100 | _triggerOpen(rq); 1101 | }, rq.reconnectInterval + 1000); 1102 | } else if (_request.reconnect && _request.fallbackTransport !== 'none') { 1103 | _reconnectWithFallbackTransport("JSONP maxReconnectOnClose reached. Downgrading to " + _request.fallbackTransport); 1104 | } else { 1105 | _onError(0, "maxReconnectOnClose reached"); 1106 | } 1107 | } 1108 | 1109 | function poll() { 1110 | var url = rq.url; 1111 | if (rq.dispatchUrl != null) { 1112 | url += rq.dispatchUrl; 1113 | } 1114 | 1115 | var data = rq.data; 1116 | if (rq.attachHeadersAsQueryString) { 1117 | url = _attachHeaders(rq); 1118 | if (data !== '') { 1119 | url += "&X-Atmosphere-Post-Body=" + encodeURIComponent(data); 1120 | } 1121 | data = ''; 1122 | } 1123 | 1124 | var head = document.head || document.getElementsByTagName("head")[0] || document.documentElement; 1125 | 1126 | script = document.createElement("script"); 1127 | script.src = url + "&jsonpTransport=" + callback; 1128 | script.clean = function () { 1129 | script.clean = script.onerror = script.onload = script.onreadystatechange = null; 1130 | if (script.parentNode) { 1131 | script.parentNode.removeChild(script); 1132 | } 1133 | 1134 | if (++request.scriptCount === 2) { 1135 | request.scriptCount = 1; 1136 | _reconnectOnFailure(); 1137 | } 1138 | 1139 | }; 1140 | script.onload = script.onreadystatechange = function () { 1141 | _debug("jsonp.onload"); 1142 | if (!script.readyState || /loaded|complete/.test(script.readyState)) { 1143 | script.clean(); 1144 | } 1145 | }; 1146 | 1147 | script.onerror = function () { 1148 | _debug("jsonp.onerror"); 1149 | request.scriptCount = 1; 1150 | script.clean(); 1151 | }; 1152 | 1153 | head.insertBefore(script, head.firstChild); 1154 | } 1155 | 1156 | // Attaches callback 1157 | window[callback] = function (msg) { 1158 | _debug("jsonp.window"); 1159 | request.scriptCount = 0; 1160 | if (rq.reconnect && rq.maxRequest === -1 || rq.requestCount++ < rq.maxRequest) { 1161 | 1162 | // _readHeaders(_jqxhr, rq); 1163 | if (!rq.executeCallbackBeforeReconnect) { 1164 | _reconnect(_jqxhr, rq, rq.pollingInterval); 1165 | } 1166 | 1167 | if (msg != null && typeof msg !== 'string') { 1168 | try { 1169 | msg = msg.message; 1170 | } catch (err) { 1171 | // The message was partial 1172 | } 1173 | } 1174 | var skipCallbackInvocation = _trackMessageSize(msg, rq, _response); 1175 | if (!skipCallbackInvocation) { 1176 | _prepareCallback(_response.responseBody, "messageReceived", 200, rq.transport); 1177 | } 1178 | 1179 | if (rq.executeCallbackBeforeReconnect) { 1180 | _reconnect(_jqxhr, rq, rq.pollingInterval); 1181 | } 1182 | _timeout(rq); 1183 | } else { 1184 | atmosphere.util.log(_request.logLevel, ["JSONP reconnect maximum try reached " + _request.requestCount]); 1185 | _onError(0, "maxRequest reached"); 1186 | } 1187 | }; 1188 | setTimeout(function () { 1189 | poll(); 1190 | }, 50); 1191 | }, 1192 | abort: function () { 1193 | if (script && script.clean) { 1194 | script.clean(); 1195 | } 1196 | } 1197 | }; 1198 | _jqxhr.open(); 1199 | } 1200 | 1201 | /** 1202 | * Build websocket object. 1203 | * 1204 | * @param location {string} Web socket url. 1205 | * @returns {websocket} Web socket object. 1206 | * @private 1207 | */ 1208 | function _getWebSocket(location) { 1209 | if (_request.webSocketImpl != null) { 1210 | return _request.webSocketImpl; 1211 | } else { 1212 | if (window.WebSocket) { 1213 | return new WebSocket(location); 1214 | } else { 1215 | return new MozWebSocket(location); 1216 | } 1217 | } 1218 | } 1219 | 1220 | /** 1221 | * Build web socket url from request url. 1222 | * 1223 | * @return {string} Web socket url (start with "ws" or "wss" for secure web socket). 1224 | * @private 1225 | */ 1226 | function _buildWebSocketUrl() { 1227 | return _attachHeaders(_request, atmosphere.util.getAbsoluteURL(_request.webSocketUrl || _request.url)).replace(/^http/, "ws"); 1228 | } 1229 | 1230 | /** 1231 | * Build SSE url from request url. 1232 | * 1233 | * @return a url with Atmosphere's headers 1234 | * @private 1235 | */ 1236 | function _buildSSEUrl() { 1237 | var url = _attachHeaders(_request); 1238 | return url; 1239 | } 1240 | 1241 | /** 1242 | * Open SSE.
1243 | * Automatically use fallback transport if SSE can't be opened. 1244 | * 1245 | * @private 1246 | */ 1247 | function _executeSSE(sseOpened) { 1248 | 1249 | _response.transport = "sse"; 1250 | 1251 | var location = _buildSSEUrl(); 1252 | 1253 | if (_canLog('debug')) { 1254 | atmosphere.util.debug("Invoking executeSSE"); 1255 | atmosphere.util.debug("Using URL: " + location); 1256 | } 1257 | 1258 | if (sseOpened && !_request.reconnect) { 1259 | if (_sse != null) { 1260 | _clearState(); 1261 | } 1262 | return; 1263 | } 1264 | 1265 | try { 1266 | _sse = new EventSource(location, { 1267 | withCredentials: _request.withCredentials 1268 | }); 1269 | } catch (e) { 1270 | _onError(0, e); 1271 | _reconnectWithFallbackTransport("SSE failed. Downgrading to fallback transport and resending"); 1272 | return; 1273 | } 1274 | 1275 | if (_request.connectTimeout > 0) { 1276 | _request.id = setTimeout(function () { 1277 | if (!sseOpened) { 1278 | _clearState(); 1279 | } 1280 | }, _request.connectTimeout); 1281 | } 1282 | 1283 | _sse.onopen = function () { 1284 | _debug("sse.onopen"); 1285 | _timeout(_request); 1286 | if (_canLog('debug')) { 1287 | atmosphere.util.debug("SSE successfully opened"); 1288 | } 1289 | 1290 | if (!_request.enableProtocol) { 1291 | if (!sseOpened) { 1292 | _open('opening', "sse", _request); 1293 | } else { 1294 | _open('re-opening', "sse", _request); 1295 | } 1296 | } else if (_request.isReopen) { 1297 | _request.isReopen = false; 1298 | _open('re-opening', _request.transport, _request); 1299 | } 1300 | 1301 | sseOpened = true; 1302 | 1303 | if (_request.method === 'POST') { 1304 | _response.state = "messageReceived"; 1305 | _push(_request.data, _request); 1306 | } 1307 | }; 1308 | 1309 | _sse.onmessage = function (message) { 1310 | _debug("sse.onmessage"); 1311 | _timeout(_request); 1312 | 1313 | if (!_request.enableXDR && window.location.host && message.origin && message.origin !== window.location.protocol + "//" + window.location.host) { 1314 | atmosphere.util.log(_request.logLevel, ["Origin was not " + window.location.protocol + "//" + window.location.host]); 1315 | return; 1316 | } 1317 | 1318 | _response.state = 'messageReceived'; 1319 | _response.status = 200; 1320 | 1321 | message = message.data; 1322 | var skipCallbackInvocation = _trackMessageSize(message, _request, _response); 1323 | 1324 | // https://github.com/remy/polyfills/blob/master/EventSource.js 1325 | // Since we polling. 1326 | /* if (_sse.URL) { 1327 | _sse.interval = 100; 1328 | _sse.URL = _buildSSEUrl(); 1329 | } */ 1330 | 1331 | if (!skipCallbackInvocation) { 1332 | _invokeCallback(); 1333 | _response.responseBody = ''; 1334 | _response.messages = []; 1335 | } 1336 | }; 1337 | 1338 | _sse.onerror = function () { 1339 | _debug("sse.onerror"); 1340 | clearTimeout(_request.id); 1341 | 1342 | if (_request.heartbeatTimer) { 1343 | clearTimeout(_request.heartbeatTimer); 1344 | } 1345 | 1346 | if (_response.closedByClientTimeout) { 1347 | return; 1348 | } 1349 | 1350 | _invokeClose(sseOpened); 1351 | _clearState(); 1352 | 1353 | if (_abortingConnection) { 1354 | atmosphere.util.log(_request.logLevel, ["SSE closed normally"]); 1355 | } else if (!sseOpened) { 1356 | _reconnectWithFallbackTransport("SSE failed. Downgrading to fallback transport and resending"); 1357 | } else if (_request.reconnect && _response.transport === 'sse') { 1358 | if (_requestCount++ < _request.maxReconnectOnClose) { 1359 | _open('re-connecting', _request.transport, _request); 1360 | if (_request.reconnectInterval > 0) { 1361 | // Prevent the online event to open a second connection while waiting for reconnect 1362 | var handleOnlineOffline = _request.handleOnlineOffline; 1363 | _request.handleOnlineOffline = false; 1364 | _request.reconnectId = setTimeout(function () { 1365 | _request.handleOnlineOffline = handleOnlineOffline; 1366 | _executeSSE(true); 1367 | }, _request.reconnectInterval); 1368 | } else { 1369 | _executeSSE(true); 1370 | } 1371 | _response.responseBody = ""; 1372 | _response.messages = []; 1373 | } else { 1374 | atmosphere.util.log(_request.logLevel, ["SSE reconnect maximum try reached " + _requestCount]); 1375 | if (_request.reconnect && _request.fallbackTransport !== 'none') { 1376 | _reconnectWithFallbackTransport("SSE maxReconnectOnClose reached. Downgrading to " + _request.fallbackTransport); 1377 | } else { 1378 | _onError(0, "maxReconnectOnClose reached"); 1379 | } 1380 | } 1381 | } 1382 | }; 1383 | } 1384 | 1385 | /** 1386 | * Open web socket.
1387 | * Automatically use fallback transport if web socket can't be opened. 1388 | * 1389 | * @private 1390 | */ 1391 | function _executeWebSocket(webSocketOpened) { 1392 | 1393 | _response.transport = "websocket"; 1394 | 1395 | var location = _buildWebSocketUrl(); 1396 | if (_canLog('debug')) { 1397 | atmosphere.util.debug("Invoking executeWebSocket, using URL: " + location); 1398 | } 1399 | 1400 | if (webSocketOpened && !_request.reconnect) { 1401 | if (_websocket != null) { 1402 | _clearState(); 1403 | } 1404 | return; 1405 | } 1406 | 1407 | _websocket = _getWebSocket(location); 1408 | if (_request.webSocketBinaryType != null) { 1409 | _websocket.binaryType = _request.webSocketBinaryType; 1410 | } 1411 | 1412 | if (_request.connectTimeout > 0) { 1413 | _request.id = setTimeout(function () { 1414 | if (!webSocketOpened) { 1415 | var _message = { 1416 | code: 1002, 1417 | reason: "Connection timeout after " + _request.connectTimeout + "ms.", 1418 | wasClean: false 1419 | }; 1420 | var socket = _websocket; 1421 | // Close it anyway 1422 | try { 1423 | _clearState(); 1424 | } catch (e) { 1425 | } 1426 | socket.onclose(_message); 1427 | } 1428 | 1429 | }, _request.connectTimeout); 1430 | } 1431 | 1432 | _websocket.onopen = function () { 1433 | if (_websocket == null) { 1434 | this.close(); 1435 | if (_request.transport == "websocket") 1436 | _close(); 1437 | return; 1438 | } 1439 | 1440 | _debug("websocket.onopen"); 1441 | if (!_request.enableProtocol || _request.connectTimeout <= 0) 1442 | _timeout(_request); 1443 | offline = false; 1444 | 1445 | if (_canLog('debug')) { 1446 | atmosphere.util.debug("Websocket successfully opened"); 1447 | } 1448 | 1449 | var reopening = webSocketOpened; 1450 | 1451 | _websocket.canSendMessage = true; 1452 | 1453 | if (!_request.enableProtocol) { 1454 | webSocketOpened = true; 1455 | if (reopening) { 1456 | _open('re-opening', "websocket", _request); 1457 | } else { 1458 | _open('opening', "websocket", _request); 1459 | } 1460 | } 1461 | 1462 | if (_request.method === 'POST') { 1463 | _response.state = "messageReceived"; 1464 | _websocket.send(_request.data); 1465 | } 1466 | }; 1467 | 1468 | _websocket.onmessage = function (message) { 1469 | if (_websocket == null) { 1470 | this.close(); 1471 | if (_request.transport == "websocket") 1472 | _close(); 1473 | return; 1474 | } 1475 | 1476 | _debug("websocket.onmessage"); 1477 | _timeout(_request); 1478 | 1479 | // We only consider it opened if we get the handshake data 1480 | // https://github.com/Atmosphere/atmosphere-javascript/issues/74 1481 | if (_request.enableProtocol) { 1482 | webSocketOpened = true; 1483 | } 1484 | 1485 | _response.state = 'messageReceived'; 1486 | _response.status = 200; 1487 | 1488 | message = message.data; 1489 | var isString = typeof (message) === 'string'; 1490 | if (isString) { 1491 | var skipCallbackInvocation = _trackMessageSize(message, _request, _response); 1492 | if (!skipCallbackInvocation) { 1493 | _invokeCallback(); 1494 | _response.responseBody = ''; 1495 | _response.messages = []; 1496 | } 1497 | } else { 1498 | message = _handleProtocol(_request, message); 1499 | if (message === "") 1500 | return; 1501 | 1502 | _response.responseBody = message; 1503 | _invokeCallback(); 1504 | _response.responseBody = null; 1505 | } 1506 | }; 1507 | 1508 | _websocket.onerror = function () { 1509 | _debug("websocket.onerror"); 1510 | if (_response.transport !== 'websocket') 1511 | return; 1512 | clearTimeout(_request.id); 1513 | 1514 | if (_request.heartbeatTimer) { 1515 | clearTimeout(_request.heartbeatTimer); 1516 | } 1517 | 1518 | _response.error = true; 1519 | }; 1520 | 1521 | _websocket.onclose = function (message) { 1522 | _debug("websocket.onclose"); 1523 | if (_response.transport !== 'websocket') 1524 | return; 1525 | clearTimeout(_request.id); 1526 | if (_response.state === 'closed') 1527 | return; 1528 | 1529 | var reason = message.reason; 1530 | if (reason === "") { 1531 | switch (message.code) { 1532 | case 1000: 1533 | reason = "Normal closure; the connection successfully completed whatever purpose for which it was created."; 1534 | break; 1535 | case 1001: 1536 | reason = "The endpoint is going away, either because of a server failure or because the " 1537 | + "browser is navigating away from the page that opened the connection."; 1538 | break; 1539 | case 1002: 1540 | reason = "The endpoint is terminating the connection due to a protocol error."; 1541 | break; 1542 | case 1003: 1543 | reason = "The connection is being terminated because the endpoint received data of a type it " 1544 | + "cannot accept (for example, a text-only endpoint received binary data)."; 1545 | break; 1546 | case 1004: 1547 | reason = "The endpoint is terminating the connection because a data frame was received that is too large."; 1548 | break; 1549 | case 1005: 1550 | reason = "Unknown: no status code was provided even though one was expected."; 1551 | break; 1552 | case 1006: 1553 | reason = "Connection was closed abnormally (that is, with no close frame being sent)."; 1554 | break; 1555 | } 1556 | } 1557 | 1558 | if (_canLog('warn')) { 1559 | atmosphere.util.warn("Websocket closed, reason: " + reason + ' - wasClean: ' + message.wasClean); 1560 | } 1561 | 1562 | if (_response.closedByClientTimeout || (_request.handleOnlineOffline && offline)) { 1563 | // IFF online/offline events are handled and we happen to be offline, we stop all reconnect attempts and 1564 | // resume them in the "online" event (if we get here in that case, something else went wrong as the 1565 | // offline handler should stop any reconnect attempt). 1566 | // 1567 | // On the other hand, if we DO NOT handle online/offline events, we continue as before with reconnecting 1568 | // even if we are offline. Failing to do so would stop all reconnect attemps forever. 1569 | if (_request.reconnectId) { 1570 | clearTimeout(_request.reconnectId); 1571 | delete _request.reconnectId; 1572 | } 1573 | return; 1574 | } 1575 | 1576 | _invokeClose(webSocketOpened); 1577 | 1578 | _response.state = 'closed'; 1579 | 1580 | if (_abortingConnection) { 1581 | atmosphere.util.log(_request.logLevel, ["Websocket closed normally"]); 1582 | } else if (_response.error && _request.curWebsocketErrorRetries < _request.maxWebsocketErrorRetries && _requestCount + 1 < _request.maxReconnectOnClose) { 1583 | _response.error = false; 1584 | _request.curWebsocketErrorRetries++; 1585 | _reconnectWebSocket(); 1586 | } else if ((_response.error || !webSocketOpened || _request.maxWebsocketErrorRetries === 0) && _request.fallbackTransport !== 'websocket') { 1587 | _response.error = false; 1588 | _reconnectWithFallbackTransport("Websocket failed on first connection attempt. Downgrading to " + _request.fallbackTransport + " and resending"); 1589 | } else if (_request.reconnect) { 1590 | _reconnectWebSocket(); 1591 | } 1592 | }; 1593 | 1594 | var ua = navigator.userAgent.toLowerCase(); 1595 | var isAndroid = ua.indexOf("android") > -1; 1596 | if (isAndroid && _websocket.url === undefined) { 1597 | // Android 4.1 does not really support websockets and fails silently 1598 | _websocket.onclose({ 1599 | reason: "Android 4.1 does not support websockets.", 1600 | wasClean: false 1601 | }); 1602 | } 1603 | } 1604 | 1605 | function _handleProtocol(request, message) { 1606 | 1607 | var nMessage = message; 1608 | if (request.transport === 'polling') return nMessage; 1609 | 1610 | if (request.enableProtocol && request.firstMessage && atmosphere.util.trim(message).length !== 0) { 1611 | var pos = request.trackMessageLength ? 1 : 0; 1612 | var messages = message.split(request.messageDelimiter); 1613 | 1614 | if (messages.length <= pos + 1) { 1615 | // Something went wrong, normally with IE or when a message is written before the 1616 | // handshake has been received. 1617 | return nMessage; 1618 | } 1619 | 1620 | request.firstMessage = false; 1621 | request.uuid = atmosphere.util.trim(messages[pos]); 1622 | 1623 | if (messages.length <= pos + 2) { 1624 | atmosphere.util.log('error', ["Protocol data not sent by the server. " + 1625 | "If you enable protocol on client side, be sure to install JavascriptProtocol interceptor on server side." + 1626 | "Also note that atmosphere-runtime 2.2+ should be used."]); 1627 | } 1628 | 1629 | _heartbeatInterval = parseInt(atmosphere.util.trim(messages[pos + 1]), 10); 1630 | _heartbeatPadding = messages[pos + 2]; 1631 | 1632 | if (request.transport !== 'long-polling') { 1633 | _triggerOpen(request); 1634 | } 1635 | uuid = request.uuid; 1636 | nMessage = ""; 1637 | 1638 | // We have trailing messages 1639 | pos = request.trackMessageLength ? 4 : 3; 1640 | if (messages.length > pos + 1) { 1641 | for (var i = pos; i < messages.length; i++) { 1642 | nMessage += messages[i]; 1643 | if (i + 1 !== messages.length) { 1644 | nMessage += request.messageDelimiter; 1645 | } 1646 | } 1647 | } 1648 | 1649 | if (request.ackInterval !== 0) { 1650 | setTimeout(function () { 1651 | _push("...ACK...", request); 1652 | }, request.ackInterval); 1653 | } 1654 | } else if (request.enableProtocol && request.firstMessage && atmosphere.util.browser.msie && +atmosphere.util.browser.version.split(".")[0] < 10) { 1655 | // In case we are getting some junk from IE 1656 | atmosphere.util.log(_request.logLevel, ["Receiving unexpected data from IE"]); 1657 | } else { 1658 | _triggerOpen(request); 1659 | } 1660 | return nMessage; 1661 | } 1662 | 1663 | function _timeout(_request) { 1664 | clearTimeout(_request.id); 1665 | if (_request.timeout > 0 && _request.transport !== 'polling') { 1666 | _request.id = setTimeout(function () { 1667 | _onClientTimeout(_request); 1668 | _disconnect(); 1669 | _clearState(); 1670 | }, _request.timeout); 1671 | } 1672 | } 1673 | 1674 | function _onClientTimeout(_request) { 1675 | _response.closedByClientTimeout = true; 1676 | _response.state = 'closedByClient'; 1677 | _response.responseBody = ""; 1678 | _response.status = 408; 1679 | _response.messages = []; 1680 | _invokeCallback(); 1681 | } 1682 | 1683 | function _onError(code, reason) { 1684 | _clearState(); 1685 | clearTimeout(_request.id); 1686 | _response.state = 'error'; 1687 | _response.reasonPhrase = reason; 1688 | _response.responseBody = ""; 1689 | _response.status = code; 1690 | _response.messages = []; 1691 | _invokeCallback(); 1692 | } 1693 | 1694 | /** 1695 | * Track received message and make sure callbacks/functions are only invoked when the complete message has been received. 1696 | * 1697 | * @param message 1698 | * @param request 1699 | * @param response 1700 | */ 1701 | function _trackMessageSize(message, request, response) { 1702 | message = _handleProtocol(request, message); 1703 | if (message.length === 0) 1704 | return true; 1705 | 1706 | response.responseBody = message; 1707 | 1708 | if (request.trackMessageLength) { 1709 | // prepend partialMessage if any 1710 | message = response.partialMessage + message; 1711 | 1712 | var messages = []; 1713 | var messageStart = message.indexOf(request.messageDelimiter); 1714 | if (messageStart != -1) { 1715 | while (messageStart !== -1) { 1716 | var str = message.substring(0, messageStart); 1717 | var messageLength = +str; 1718 | if (isNaN(messageLength)) { 1719 | // Discard partial message, otherwise it would never recover from this condition 1720 | response.partialMessage = ''; 1721 | throw new Error('message length "' + str + '" is not a number'); 1722 | } 1723 | messageStart += request.messageDelimiter.length; 1724 | if (messageStart + messageLength > message.length) { 1725 | // message not complete, so there is no trailing messageDelimiter 1726 | messageStart = -1; 1727 | } else { 1728 | // message complete, so add it 1729 | messages.push(message.substring(messageStart, messageStart + messageLength)); 1730 | // remove consumed characters 1731 | message = message.substring(messageStart + messageLength, message.length); 1732 | messageStart = message.indexOf(request.messageDelimiter); 1733 | } 1734 | } 1735 | 1736 | /* keep any remaining data */ 1737 | response.partialMessage = message; 1738 | 1739 | if (messages.length !== 0) { 1740 | response.responseBody = messages.join(request.messageDelimiter); 1741 | response.messages = messages; 1742 | return false; 1743 | } else { 1744 | response.responseBody = ""; 1745 | response.messages = []; 1746 | return true; 1747 | } 1748 | } 1749 | } 1750 | response.responseBody = message; 1751 | response.messages = [message]; 1752 | return false; 1753 | } 1754 | 1755 | function _reconnectWebSocket() { 1756 | _clearState(); 1757 | if (_requestCount++ < _request.maxReconnectOnClose) { 1758 | _open('re-connecting', _request.transport, _request); 1759 | if (_request.reconnectInterval > 0) { 1760 | // Prevent the online event to open a second connection while waiting for reconnect 1761 | var handleOnlineOffline = _request.handleOnlineOffline; 1762 | _request.handleOnlineOffline = false; 1763 | _request.reconnectId = setTimeout(function () { 1764 | _request.handleOnlineOffline = handleOnlineOffline; 1765 | _response.responseBody = ""; 1766 | _response.messages = []; 1767 | _executeWebSocket(true); 1768 | }, _request.reconnectInterval); 1769 | } else { 1770 | _response.responseBody = ""; 1771 | _response.messages = []; 1772 | _executeWebSocket(true); 1773 | } 1774 | } else { 1775 | atmosphere.util.log(_request.logLevel, ["Websocket reconnect maximum try reached " + _requestCount]); 1776 | if (_request.reconnect && _request.fallbackTransport !== 'none') { 1777 | _reconnectWithFallbackTransport("Websocket maxReconnectOnClose reached. Downgrading to " + _request.fallbackTransport); 1778 | } else { 1779 | _onError(0, "maxReconnectOnClose reached"); 1780 | } 1781 | } 1782 | } 1783 | 1784 | /** 1785 | * Reconnect request with fallback transport.
1786 | * Used in case websocket can't be opened. 1787 | * 1788 | * @private 1789 | */ 1790 | function _reconnectWithFallbackTransport(errorMessage) { 1791 | atmosphere.util.log(_request.logLevel, [errorMessage]); 1792 | 1793 | _clearState(); 1794 | 1795 | if (typeof (_request.onTransportFailure) !== 'undefined') { 1796 | _request.onTransportFailure(errorMessage, _request); 1797 | } 1798 | 1799 | if (_request.reconnect && _request.transport !== 'none' || _request.transport == null) { 1800 | _request.transport = _request.fallbackTransport; 1801 | _request.method = _request.fallbackMethod; 1802 | _response.transport = _request.fallbackTransport; 1803 | _response.state = ''; 1804 | _request.fallbackTransport = 'none'; 1805 | if (_request.reconnectInterval > 0) { 1806 | // Prevent the online event to open a second connection while waiting for reconnect 1807 | var handleOnlineOffline = _request.handleOnlineOffline; 1808 | _request.handleOnlineOffline = false; 1809 | _request.reconnectId = setTimeout(function () { 1810 | _request.handleOnlineOffline = handleOnlineOffline; 1811 | _execute(); 1812 | }, _request.reconnectInterval); 1813 | } else { 1814 | _execute(); 1815 | } 1816 | } else { 1817 | _onError(500, "Unable to reconnect with fallback transport"); 1818 | } 1819 | } 1820 | 1821 | /** 1822 | * Get url from request and attach headers to it. 1823 | * 1824 | * @param request {Object} request Request parameters, if undefined _request object will be used. 1825 | * 1826 | * @returns {Object} Request object, if undefined, _request object will be used. 1827 | * @private 1828 | */ 1829 | function _attachHeaders(request, url) { 1830 | var rq = _request; 1831 | if ((request != null) && (typeof (request) !== 'undefined')) { 1832 | rq = request; 1833 | } 1834 | 1835 | if (url == null) { 1836 | url = rq.url; 1837 | } 1838 | 1839 | // If not enabled 1840 | if (!rq.attachHeadersAsQueryString) 1841 | return url; 1842 | 1843 | // If already added 1844 | if (url.indexOf("X-Atmosphere-Framework") !== -1) { 1845 | return url; 1846 | } 1847 | 1848 | url += (url.indexOf('?') !== -1) ? '&' : '?'; 1849 | url += "X-Atmosphere-tracking-id=" + rq.uuid; 1850 | url += "&X-Atmosphere-Framework=" + atmosphere.version; 1851 | url += "&X-Atmosphere-Transport=" + rq.transport; 1852 | 1853 | if (rq.trackMessageLength) { 1854 | url += "&X-Atmosphere-TrackMessageSize=" + "true"; 1855 | } 1856 | 1857 | if (rq.heartbeat !== null && rq.heartbeat.server !== null) { 1858 | url += "&X-Heartbeat-Server=" + rq.heartbeat.server; 1859 | } 1860 | 1861 | if (rq.contentType !== '') { 1862 | //Eurk! 1863 | url += "&Content-Type=" + (rq.transport === 'websocket' ? rq.contentType : encodeURIComponent(rq.contentType)); 1864 | } 1865 | 1866 | if (rq.enableProtocol) { 1867 | url += "&X-atmo-protocol=true"; 1868 | } 1869 | 1870 | atmosphere.util.each(rq.headers, function (name, value) { 1871 | var h = atmosphere.util.isFunction(value) ? value.call(this, rq, request, _response) : value; 1872 | if (h != null) { 1873 | url += "&" + encodeURIComponent(name) + "=" + encodeURIComponent(h); 1874 | } 1875 | }); 1876 | 1877 | return url; 1878 | } 1879 | 1880 | function _triggerOpen(rq) { 1881 | if (!rq.isOpen) { 1882 | rq.isOpen = true; 1883 | _open('opening', rq.transport, rq); 1884 | } else if (rq.isReopen) { 1885 | rq.isReopen = false; 1886 | _open('re-opening', rq.transport, rq); 1887 | } else if (_response.state === 'messageReceived' && (rq.transport === 'jsonp' || rq.transport === 'long-polling')) { 1888 | _openAfterResume(_response); 1889 | } else { 1890 | return; 1891 | } 1892 | 1893 | _startHeartbeat(rq); 1894 | } 1895 | 1896 | function _startHeartbeat(rq) { 1897 | if (rq.heartbeatTimer != null) { 1898 | clearTimeout(rq.heartbeatTimer); 1899 | } 1900 | 1901 | if (!isNaN(_heartbeatInterval) && _heartbeatInterval > 0) { 1902 | var _pushHeartbeat = function () { 1903 | if (_canLog('debug')) { 1904 | atmosphere.util.debug("Sending heartbeat"); 1905 | } 1906 | _push(_heartbeatPadding, _request); 1907 | rq.heartbeatTimer = setTimeout(_pushHeartbeat, _heartbeatInterval); 1908 | }; 1909 | rq.heartbeatTimer = setTimeout(_pushHeartbeat, _heartbeatInterval); 1910 | } 1911 | } 1912 | 1913 | /** 1914 | * Execute ajax request.
1915 | * 1916 | * @param request {Object} request Request parameters, if undefined _request object will be used. 1917 | * @private 1918 | */ 1919 | function _executeRequest(request) { 1920 | var rq = _request; 1921 | if ((request != null) || (typeof (request) !== 'undefined')) { 1922 | rq = request; 1923 | } 1924 | 1925 | rq.lastIndex = 0; 1926 | rq.readyState = 0; 1927 | 1928 | // CORS fake using JSONP 1929 | if ((rq.transport === 'jsonp') || ((rq.enableXDR) && (atmosphere.util.checkCORSSupport()))) { 1930 | _jsonp(rq); 1931 | return; 1932 | } 1933 | 1934 | if (atmosphere.util.browser.msie && +atmosphere.util.browser.version.split(".")[0] < 10) { 1935 | if ((rq.transport === 'streaming')) { 1936 | if (rq.enableXDR && window.XDomainRequest) { 1937 | _ieXDR(rq); 1938 | } else { 1939 | _ieStreaming(rq); 1940 | } 1941 | return; 1942 | } 1943 | 1944 | if ((rq.enableXDR) && (window.XDomainRequest)) { 1945 | _ieXDR(rq); 1946 | return; 1947 | } 1948 | } 1949 | 1950 | var reconnectFExec = function (force) { 1951 | rq.lastIndex = 0; 1952 | _requestCount++; // Increase also when forcing reconnect as _open checks _requestCount 1953 | if (force || (rq.reconnect && _requestCount <= rq.maxReconnectOnClose)) { 1954 | var delay = force ? 0 : request.reconnectInterval; // Reconnect immediately if the server resumed the connection (timeout) 1955 | _response.ffTryingReconnect = true; 1956 | _open('re-connecting', request.transport, request); 1957 | _reconnect(ajaxRequest, rq, delay); 1958 | } else if (_request.reconnect && _request.fallbackTransport !== 'none') { 1959 | _reconnectWithFallbackTransport("maxReconnectOnClose reached. Downgrading to " + _request.fallbackTransport); 1960 | } else { 1961 | _onError(0, "maxReconnectOnClose reached"); 1962 | } 1963 | }; 1964 | 1965 | var reconnectF = function (force) { 1966 | if (_beforeUnloadState) { 1967 | // ATMOSPHERE-JAVASCRIPT-143: Delay reconnect to avoid reconnect attempts before an actual unload (we don't know if an unload will happen, yet) 1968 | atmosphere.util.debug(new Date() + " Atmosphere: reconnectF: execution delayed due to _beforeUnloadState flag"); 1969 | setTimeout(function () { 1970 | reconnectFExec(force); 1971 | }, 5000); 1972 | } else { 1973 | reconnectFExec(force); 1974 | } 1975 | }; 1976 | 1977 | var disconnected = function () { 1978 | // Prevent onerror callback to be called 1979 | _response.errorHandled = true; 1980 | _clearState(); 1981 | reconnectF(false); 1982 | }; 1983 | 1984 | if (rq.force || (rq.reconnect && (rq.maxRequest === -1 || rq.requestCount++ < rq.maxRequest))) { 1985 | rq.force = false; 1986 | 1987 | var ajaxRequest = atmosphere.util.xhr(); 1988 | ajaxRequest.hasData = false; 1989 | 1990 | _doRequest(ajaxRequest, rq, true); 1991 | 1992 | if (rq.suspend) { 1993 | _activeRequest = ajaxRequest; 1994 | } 1995 | 1996 | if (rq.transport !== 'polling') { 1997 | _response.transport = rq.transport; 1998 | 1999 | ajaxRequest.onabort = function () { 2000 | _debug("ajaxrequest.onabort") 2001 | _invokeClose(true); 2002 | }; 2003 | 2004 | ajaxRequest.onerror = function () { 2005 | _debug("ajaxrequest.onerror") 2006 | _response.error = true; 2007 | _response.ffTryingReconnect = true; 2008 | try { 2009 | _response.status = ajaxRequest.status; 2010 | } catch (e) { 2011 | _response.status = 500; 2012 | } 2013 | 2014 | if (!_response.status) { 2015 | _response.status = 500; 2016 | } 2017 | if (!_response.errorHandled) { 2018 | _clearState(); 2019 | reconnectF(false); 2020 | } 2021 | }; 2022 | } 2023 | 2024 | ajaxRequest.onreadystatechange = function () { 2025 | _debug("ajaxRequest.onreadystatechange, new state: " + ajaxRequest.readyState); 2026 | if (_abortingConnection) { 2027 | _debug("onreadystatechange has been ignored due to _abortingConnection flag"); 2028 | return; 2029 | } 2030 | 2031 | _response.error = false; 2032 | var skipCallbackInvocation = false; 2033 | var update = false; 2034 | 2035 | if (rq.transport === 'streaming' && rq.readyState > 2 && ajaxRequest.readyState === 4) { 2036 | _clearState(); 2037 | reconnectF(false); 2038 | return; 2039 | } 2040 | 2041 | rq.readyState = ajaxRequest.readyState; 2042 | 2043 | if (rq.transport === 'streaming' && ajaxRequest.readyState >= 3) { 2044 | update = true; 2045 | } else if (rq.transport === 'long-polling' && ajaxRequest.readyState === 4) { 2046 | update = true; 2047 | } 2048 | _timeout(_request); 2049 | 2050 | if (rq.transport !== 'polling') { 2051 | // MSIE 9 and lower status can be higher than 1000, Chrome can be 0 2052 | var status = 200; 2053 | if (ajaxRequest.readyState === 4) { 2054 | status = ajaxRequest.status > 1000 ? 0 : ajaxRequest.status; 2055 | } 2056 | 2057 | if (!rq.reconnectOnServerError && (status >= 300 && status < 600)) { 2058 | _onError(status, ajaxRequest.statusText); 2059 | return; 2060 | } 2061 | 2062 | if (status >= 300 || status === 0) { 2063 | if (!rq.isOpen && _canLog('warn')) { 2064 | atmosphere.util.warn(rq.transport + " connection failed with status: " + status + " " + (ajaxRequest.statusText || "Unable to connect")); 2065 | } 2066 | disconnected(); 2067 | return; 2068 | } 2069 | 2070 | // Firefox incorrectly send statechange 0->2 when a reconnect attempt fails. The above checks ensure that onopen is not called for these 2071 | if ((!rq.enableProtocol || !request.firstMessage) && (ajaxRequest.readyState === 2 || ajaxRequest.readyState > 2 && !rq.isOpen)) { 2072 | // In that case, ajaxRequest.onerror will be called just after onreadystatechange is called, so we delay the trigger until we are 2073 | // guarantee the connection is well established. 2074 | if (atmosphere.util.browser.mozilla && _response.ffTryingReconnect) { 2075 | _response.ffTryingReconnect = false; 2076 | setTimeout(function () { 2077 | if (!_response.ffTryingReconnect) { 2078 | _triggerOpen(rq); 2079 | } 2080 | }, 500); 2081 | } else { 2082 | _triggerOpen(rq); 2083 | } 2084 | } 2085 | 2086 | } else if (ajaxRequest.readyState === 4) { 2087 | update = true; 2088 | } 2089 | 2090 | if (update) { 2091 | var responseText = ajaxRequest.responseText; 2092 | _response.errorHandled = false; 2093 | 2094 | // IE behave the same way when resuming long-polling or when the server goes down. 2095 | if (rq.transport === 'long-polling' && atmosphere.util.trim(responseText).length === 0) { 2096 | // For browser that aren't support onabort 2097 | if (!ajaxRequest.hasData) { 2098 | reconnectF(true); 2099 | } else { 2100 | ajaxRequest.hasData = false; 2101 | } 2102 | return; 2103 | } 2104 | ajaxRequest.hasData = true; 2105 | 2106 | _readHeaders(ajaxRequest, _request); 2107 | 2108 | if (rq.transport === 'streaming') { 2109 | if (!atmosphere.util.browser.opera) { 2110 | var message = responseText.substring(rq.lastIndex, responseText.length); 2111 | skipCallbackInvocation = _trackMessageSize(message, rq, _response); 2112 | 2113 | rq.lastIndex = responseText.length; 2114 | if (skipCallbackInvocation) { 2115 | return; 2116 | } 2117 | } else { 2118 | atmosphere.util.iterate(function () { 2119 | if (_response.status !== 500 && ajaxRequest.responseText.length > rq.lastIndex) { 2120 | try { 2121 | _response.status = ajaxRequest.status; 2122 | _response.headers = atmosphere.util.parseHeaders(ajaxRequest.getAllResponseHeaders()); 2123 | 2124 | _readHeaders(ajaxRequest, _request); 2125 | 2126 | } catch (e) { 2127 | _response.status = 404; 2128 | } 2129 | _timeout(_request); 2130 | 2131 | _response.state = "messageReceived"; 2132 | var message = ajaxRequest.responseText.substring(rq.lastIndex); 2133 | rq.lastIndex = ajaxRequest.responseText.length; 2134 | 2135 | skipCallbackInvocation = _trackMessageSize(message, rq, _response); 2136 | if (!skipCallbackInvocation) { 2137 | _invokeCallback(); 2138 | } 2139 | 2140 | if (_verifyStreamingLength(ajaxRequest, rq)) { 2141 | _reconnectOnMaxStreamingLength(ajaxRequest, rq); 2142 | return; 2143 | } 2144 | } else if (_response.status > 400) { 2145 | // Prevent replaying the last message. 2146 | rq.lastIndex = ajaxRequest.responseText.length; 2147 | return false; 2148 | } 2149 | }, 0); 2150 | } 2151 | } else { 2152 | skipCallbackInvocation = _trackMessageSize(responseText, rq, _response); 2153 | } 2154 | var closeStream = _verifyStreamingLength(ajaxRequest, rq); 2155 | 2156 | try { 2157 | _response.status = ajaxRequest.status; 2158 | _response.headers = atmosphere.util.parseHeaders(ajaxRequest.getAllResponseHeaders()); 2159 | 2160 | _readHeaders(ajaxRequest, rq); 2161 | } catch (e) { 2162 | _response.status = 404; 2163 | } 2164 | 2165 | if (rq.suspend) { 2166 | _response.state = _response.status === 0 ? "closed" : "messageReceived"; 2167 | } else { 2168 | _response.state = "messagePublished"; 2169 | } 2170 | 2171 | var isAllowedToReconnect = !closeStream && request.transport !== 'streaming' && request.transport !== 'polling'; 2172 | if (isAllowedToReconnect && !rq.executeCallbackBeforeReconnect) { 2173 | _reconnect(ajaxRequest, rq, rq.pollingInterval); 2174 | } 2175 | 2176 | if (_response.responseBody.length !== 0 && !skipCallbackInvocation) 2177 | _invokeCallback(); 2178 | 2179 | if (isAllowedToReconnect && rq.executeCallbackBeforeReconnect) { 2180 | _reconnect(ajaxRequest, rq, rq.pollingInterval); 2181 | } 2182 | 2183 | if (closeStream) { 2184 | _reconnectOnMaxStreamingLength(ajaxRequest, rq); 2185 | } 2186 | } 2187 | }; 2188 | 2189 | try { 2190 | ajaxRequest.send(rq.data); 2191 | _subscribed = true; 2192 | } catch (e) { 2193 | atmosphere.util.log(rq.logLevel, ["Unable to connect to " + rq.url]); 2194 | _onError(0, e); 2195 | } 2196 | 2197 | } else { 2198 | if (rq.logLevel === 'debug') { 2199 | atmosphere.util.log(rq.logLevel, ["Max re-connection reached."]); 2200 | } 2201 | _onError(0, "maxRequest reached"); 2202 | } 2203 | } 2204 | 2205 | function _reconnectOnMaxStreamingLength(ajaxRequest, rq) { 2206 | _response.messages = []; 2207 | rq.isReopen = true; 2208 | _close(); 2209 | _abortingConnection = false; 2210 | _reconnect(ajaxRequest, rq, 500); 2211 | } 2212 | 2213 | /** 2214 | * Do ajax request. 2215 | * 2216 | * @param ajaxRequest Ajax request. 2217 | * @param request Request parameters. 2218 | * @param create If ajax request has to be open. 2219 | */ 2220 | function _doRequest(ajaxRequest, request, create) { 2221 | // Prevent Android to cache request 2222 | var url = request.url; 2223 | if (request.dispatchUrl != null && request.method === 'POST') { 2224 | url += request.dispatchUrl; 2225 | } 2226 | url = _attachHeaders(request, url); 2227 | url = atmosphere.util.prepareURL(url); 2228 | 2229 | if (create) { 2230 | ajaxRequest.open(request.method, url, true); 2231 | if (request.connectTimeout > 0) { 2232 | request.id = setTimeout(function () { 2233 | if (request.requestCount === 0) { 2234 | _clearState(); 2235 | _prepareCallback("Connect timeout", "closed", 200, request.transport); 2236 | } 2237 | }, request.connectTimeout); 2238 | } 2239 | } 2240 | 2241 | if (_request.withCredentials && _request.transport !== 'websocket') { 2242 | if ("withCredentials" in ajaxRequest) { 2243 | ajaxRequest.withCredentials = true; 2244 | } 2245 | } 2246 | 2247 | if (!_request.dropHeaders) { 2248 | ajaxRequest.setRequestHeader("X-Atmosphere-Framework", atmosphere.version); 2249 | ajaxRequest.setRequestHeader("X-Atmosphere-Transport", request.transport); 2250 | 2251 | if (request.heartbeat !== null && request.heartbeat.server !== null) { 2252 | ajaxRequest.setRequestHeader("X-Heartbeat-Server", ajaxRequest.heartbeat.server); 2253 | } 2254 | 2255 | if (request.trackMessageLength) { 2256 | ajaxRequest.setRequestHeader("X-Atmosphere-TrackMessageSize", "true"); 2257 | } 2258 | ajaxRequest.setRequestHeader("X-Atmosphere-tracking-id", request.uuid); 2259 | 2260 | atmosphere.util.each(request.headers, function (name, value) { 2261 | var h = atmosphere.util.isFunction(value) ? value.call(this, ajaxRequest, request, create, _response) : value; 2262 | if (h != null) { 2263 | ajaxRequest.setRequestHeader(name, h); 2264 | } 2265 | }); 2266 | } 2267 | 2268 | if (request.contentType !== '') { 2269 | ajaxRequest.setRequestHeader("Content-Type", request.contentType); 2270 | } 2271 | } 2272 | 2273 | function _reconnect(ajaxRequest, request, delay) { 2274 | 2275 | if (_response.closedByClientTimeout) { 2276 | return; 2277 | } 2278 | 2279 | if (request.reconnect || (request.suspend && _subscribed)) { 2280 | var status = 0; 2281 | if (ajaxRequest && ajaxRequest.readyState > 1) { 2282 | status = ajaxRequest.status > 1000 ? 0 : ajaxRequest.status; 2283 | } 2284 | _response.status = status === 0 ? 204 : status; 2285 | _response.reasonPhrase = status === 0 ? "Server resumed the connection or down." : "OK"; 2286 | 2287 | clearTimeout(request.id); 2288 | if (request.reconnectId) { 2289 | clearTimeout(request.reconnectId); 2290 | delete request.reconnectId; 2291 | } 2292 | 2293 | if (delay > 0) { 2294 | // For whatever reason, never cancel a reconnect timeout as it is mandatory to reconnect. 2295 | // Prevent the online event to open a second connection while waiting for reconnect 2296 | var handleOnlineOffline = _request.handleOnlineOffline; 2297 | _request.handleOnlineOffline = false; 2298 | _request.reconnectId = setTimeout(function () { 2299 | _request.handleOnlineOffline = handleOnlineOffline; 2300 | _executeRequest(request); 2301 | }, delay); 2302 | } else { 2303 | _executeRequest(request); 2304 | } 2305 | } 2306 | } 2307 | 2308 | function _tryingToReconnect(response) { 2309 | response.state = 're-connecting'; 2310 | _invokeFunction(response); 2311 | } 2312 | 2313 | function _openAfterResume(response) { 2314 | response.state = 'openAfterResume'; 2315 | _invokeFunction(response); 2316 | response.state = 'messageReceived'; 2317 | } 2318 | 2319 | function _ieXDR(request) { 2320 | if (request.transport !== "polling") { 2321 | _ieStream = _configureXDR(request); 2322 | _ieStream.open(); 2323 | } else { 2324 | _configureXDR(request).open(); 2325 | } 2326 | } 2327 | 2328 | function _configureXDR(request) { 2329 | var rq = _request; 2330 | if ((request != null) && (typeof (request) !== 'undefined')) { 2331 | rq = request; 2332 | } 2333 | 2334 | var transport = rq.transport; 2335 | var lastIndex = 0; 2336 | var xdr = new window.XDomainRequest(); 2337 | var reconnect = function () { 2338 | if (rq.transport === "long-polling" && (rq.reconnect && (rq.maxRequest === -1 || rq.requestCount++ < rq.maxRequest))) { 2339 | xdr.status = 200; 2340 | _ieXDR(rq); 2341 | } 2342 | }; 2343 | 2344 | var rewriteURL = rq.rewriteURL || function (url) { 2345 | // Maintaining session by rewriting URL 2346 | // http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url 2347 | var match = /(?:^|;\s*)(JSESSIONID|PHPSESSID)=([^;]*)/.exec(document.cookie); 2348 | 2349 | switch (match && match[1]) { 2350 | case "JSESSIONID": 2351 | return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + match[2] + "$1"); 2352 | case "PHPSESSID": 2353 | return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + match[2] + "&").replace(/&$/, ""); 2354 | } 2355 | return url; 2356 | }; 2357 | 2358 | // Handles open and message event 2359 | xdr.onprogress = function () { 2360 | handle(xdr); 2361 | }; 2362 | // Handles error event 2363 | xdr.onerror = function () { 2364 | // If the server doesn't send anything back to XDR will fail with polling 2365 | if (rq.transport !== 'polling') { 2366 | _clearState(); 2367 | if (_requestCount++ < rq.maxReconnectOnClose) { 2368 | if (rq.reconnectInterval > 0) { 2369 | rq.reconnectId = setTimeout(function () { 2370 | _open('re-connecting', request.transport, request); 2371 | _ieXDR(rq); 2372 | }, rq.reconnectInterval); 2373 | } else { 2374 | _open('re-connecting', request.transport, request); 2375 | _ieXDR(rq); 2376 | } 2377 | } else if (_request.reconnect && _request.fallbackTransport !== 'none') { 2378 | _reconnectWithFallbackTransport("maxReconnectOnClose reached. Downgrading to " + _request.fallbackTransport); 2379 | } else { 2380 | _onError(0, "maxReconnectOnClose reached"); 2381 | } 2382 | } 2383 | }; 2384 | 2385 | // Handles close event 2386 | xdr.onload = function () { 2387 | }; 2388 | 2389 | var handle = function (xdr) { 2390 | clearTimeout(rq.id); 2391 | var message = xdr.responseText; 2392 | 2393 | message = message.substring(lastIndex); 2394 | lastIndex += message.length; 2395 | 2396 | if (transport !== 'polling') { 2397 | _timeout(rq); 2398 | 2399 | var skipCallbackInvocation = _trackMessageSize(message, rq, _response); 2400 | 2401 | if (transport === 'long-polling' && atmosphere.util.trim(message).length === 0) 2402 | return; 2403 | 2404 | if (rq.executeCallbackBeforeReconnect) { 2405 | reconnect(); 2406 | } 2407 | 2408 | if (!skipCallbackInvocation) { 2409 | _prepareCallback(_response.responseBody, "messageReceived", 200, transport); 2410 | } 2411 | 2412 | if (!rq.executeCallbackBeforeReconnect) { 2413 | reconnect(); 2414 | } 2415 | } 2416 | }; 2417 | 2418 | return { 2419 | open: function () { 2420 | var url = rq.url; 2421 | if (rq.dispatchUrl != null) { 2422 | url += rq.dispatchUrl; 2423 | } 2424 | url = _attachHeaders(rq, url); 2425 | xdr.open(rq.method, rewriteURL(url)); 2426 | if (rq.method === 'GET') { 2427 | xdr.send(); 2428 | } else { 2429 | xdr.send(rq.data); 2430 | } 2431 | 2432 | if (rq.connectTimeout > 0) { 2433 | rq.id = setTimeout(function () { 2434 | if (rq.requestCount === 0) { 2435 | _clearState(); 2436 | _prepareCallback("Connect timeout", "closed", 200, rq.transport); 2437 | } 2438 | }, rq.connectTimeout); 2439 | } 2440 | }, 2441 | close: function () { 2442 | xdr.abort(); 2443 | } 2444 | }; 2445 | } 2446 | 2447 | function _ieStreaming(request) { 2448 | _ieStream = _configureIE(request); 2449 | _ieStream.open(); 2450 | } 2451 | 2452 | function _configureIE(request) { 2453 | var rq = _request; 2454 | if ((request != null) && (typeof (request) !== 'undefined')) { 2455 | rq = request; 2456 | } 2457 | 2458 | var stop; 2459 | var doc = new window.ActiveXObject("htmlfile"); 2460 | 2461 | doc.open(); 2462 | doc.close(); 2463 | 2464 | var url = rq.url; 2465 | if (rq.dispatchUrl != null) { 2466 | url += rq.dispatchUrl; 2467 | } 2468 | 2469 | if (rq.transport !== 'polling') { 2470 | _response.transport = rq.transport; 2471 | } 2472 | 2473 | return { 2474 | open: function () { 2475 | var iframe = doc.createElement("iframe"); 2476 | 2477 | url = _attachHeaders(rq); 2478 | if (rq.data !== '') { 2479 | url += "&X-Atmosphere-Post-Body=" + encodeURIComponent(rq.data); 2480 | } 2481 | 2482 | // Finally attach a timestamp to prevent Android and IE caching. 2483 | url = atmosphere.util.prepareURL(url); 2484 | 2485 | iframe.src = url; 2486 | doc.body.appendChild(iframe); 2487 | 2488 | // For the server to respond in a consistent format regardless of user agent, we polls response text 2489 | var cdoc = iframe.contentDocument || iframe.contentWindow.document; 2490 | 2491 | stop = atmosphere.util.iterate(function () { 2492 | try { 2493 | if (!cdoc.firstChild) { 2494 | return; 2495 | } 2496 | 2497 | var res = cdoc.body ? cdoc.body.lastChild : cdoc; 2498 | if (res.omgThisIsBroken) { 2499 | // Cause an exception when res is null, to trigger a reconnect... 2500 | } 2501 | var readResponse = function () { 2502 | // Clones the element not to disturb the original one 2503 | var clone = res.cloneNode(true); 2504 | 2505 | // If the last character is a carriage return or a line feed, IE ignores it in the innerText property 2506 | // therefore, we add another non-newline character to preserve it 2507 | clone.appendChild(cdoc.createTextNode(".")); 2508 | 2509 | var text = clone.innerText; 2510 | 2511 | text = text.substring(0, text.length - 1); 2512 | return text; 2513 | 2514 | }; 2515 | 2516 | // To support text/html content type 2517 | if (!cdoc.body || !cdoc.body.firstChild || cdoc.body.firstChild.nodeName.toLowerCase() !== "pre") { 2518 | // Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped 2519 | // it is deprecated in HTML5, but still works 2520 | var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement || cdoc; 2521 | var script = cdoc.createElement("script"); 2522 | 2523 | script.text = "document.write('')"; 2524 | 2525 | head.insertBefore(script, head.firstChild); 2526 | head.removeChild(script); 2527 | 2528 | // The plaintext element will be the response container 2529 | res = cdoc.body.lastChild; 2530 | } 2531 | 2532 | if (rq.closed) { 2533 | rq.isReopen = true; 2534 | } 2535 | 2536 | // Handles message and close event 2537 | stop = atmosphere.util.iterate(function () { 2538 | var text = readResponse(); 2539 | if (text.length > rq.lastIndex) { 2540 | _timeout(_request); 2541 | 2542 | _response.status = 200; 2543 | _response.error = false; 2544 | 2545 | // Empties response every time that it is handled 2546 | res.innerText = ""; 2547 | var skipCallbackInvocation = _trackMessageSize(text, rq, _response); 2548 | if (skipCallbackInvocation) { 2549 | return false; 2550 | } 2551 | 2552 | _prepareCallback(_response.responseBody, "messageReceived", 200, rq.transport); 2553 | } 2554 | 2555 | rq.lastIndex = 0; 2556 | 2557 | if (cdoc.readyState === "complete") { 2558 | _invokeClose(true); 2559 | _open('re-connecting', rq.transport, rq); 2560 | if (rq.reconnectInterval > 0) { 2561 | rq.reconnectId = setTimeout(function () { 2562 | _ieStreaming(rq); 2563 | }, rq.reconnectInterval); 2564 | } else { 2565 | _ieStreaming(rq); 2566 | } 2567 | return false; 2568 | } 2569 | }, null); 2570 | 2571 | return false; 2572 | } catch (err) { 2573 | _response.error = true; 2574 | _open('re-connecting', rq.transport, rq); 2575 | if (_requestCount++ < rq.maxReconnectOnClose) { 2576 | if (rq.reconnectInterval > 0) { 2577 | rq.reconnectId = setTimeout(function () { 2578 | _ieStreaming(rq); 2579 | }, rq.reconnectInterval); 2580 | } else { 2581 | _ieStreaming(rq); 2582 | } 2583 | } else if (_request.reconnect && _request.fallbackTransport !== 'none') { 2584 | _reconnectWithFallbackTransport("maxReconnectOnClose reached. Downgrading to " + _request.fallbackTransport); 2585 | } else { 2586 | _onError(0, "maxReconnectOnClose reached"); 2587 | } 2588 | doc.execCommand("Stop"); 2589 | doc.close(); 2590 | return false; 2591 | } 2592 | }); 2593 | }, 2594 | 2595 | close: function () { 2596 | if (stop) { 2597 | stop(); 2598 | } 2599 | 2600 | doc.execCommand("Stop"); 2601 | _invokeClose(true); 2602 | } 2603 | }; 2604 | } 2605 | 2606 | /** 2607 | * Send message. <br> 2608 | * Will be automatically dispatch to other connected. 2609 | * 2610 | * @param {Object, string} Message to send. 2611 | * @private 2612 | */ 2613 | function _push(message, _request) { 2614 | 2615 | if (_localStorageService != null) { 2616 | _pushLocal(message); 2617 | } else if (_activeRequest != null || _sse != null 2618 | // Avoid errors when sending message during long-polling reconnection 2619 | || _request && _request.isOpen && _request.reconnect && _request.transport === "long-polling") { 2620 | _pushAjaxMessage(message); 2621 | } else if (_ieStream != null) { 2622 | _pushIE(message); 2623 | } else if (_jqxhr != null) { 2624 | _pushJsonp(message); 2625 | } else if (_websocket != null) { 2626 | _pushWebSocket(message); 2627 | // Avoid errors when sending message during websocket reconnection 2628 | } else if (_request && _request.isOpen && _request.reconnect && _request.isReopen && _request.transport === "websocket") { 2629 | _debug("Waiting for the websocket reconnection to send the message..."); 2630 | var reopenHandler = _request.onReopen; 2631 | _request.onReopen = function() { 2632 | _request.onReopen = reopenHandler; 2633 | if (typeof (reopenHandler) !== 'undefined') { 2634 | reopenHandler.apply(this, arguments); 2635 | } 2636 | _push(message, _request); 2637 | } 2638 | } else { 2639 | _onError(0, "No suspended connection available"); 2640 | atmosphere.util.error("No suspended connection available. Make sure atmosphere.subscribe has been called and request.onOpen invoked before trying to push data"); 2641 | } 2642 | } 2643 | 2644 | function _pushOnClose(message, rq) { 2645 | if (!rq) { 2646 | rq = _getPushRequest(message); 2647 | } 2648 | rq.transport = "polling"; 2649 | rq.method = "GET"; 2650 | rq.withCredentials = false; 2651 | rq.reconnect = false; 2652 | rq.force = true; 2653 | rq.suspend = false; 2654 | rq.timeout = 1000; 2655 | if (_request.unloadBackwardCompat) { 2656 | _executeRequest(rq); 2657 | } else { 2658 | navigator.sendBeacon(rq.url, rq.data); 2659 | } 2660 | } 2661 | 2662 | function _pushLocal(message) { 2663 | _localStorageService.send(message); 2664 | } 2665 | 2666 | function _intraPush(message) { 2667 | // IE 9 will crash if not. 2668 | if (message.length === 0) 2669 | return; 2670 | 2671 | try { 2672 | if (_localStorageService) { 2673 | _localStorageService.localSend(message); 2674 | } else if (_storageService) { 2675 | _storageService.signal("localMessage", JSON.stringify({ 2676 | id: guid, 2677 | event: message 2678 | })); 2679 | } 2680 | } catch (err) { 2681 | atmosphere.util.error(err); 2682 | } 2683 | } 2684 | 2685 | /** 2686 | * Send a message using currently opened ajax request (using http-streaming or long-polling). <br> 2687 | * 2688 | * @param {string, Object} Message to send. This is an object, string message is saved in data member. 2689 | * @private 2690 | */ 2691 | function _pushAjaxMessage(message) { 2692 | var rq = _getPushRequest(message); 2693 | _executeRequest(rq); 2694 | } 2695 | 2696 | /** 2697 | * Send a message using currently opened ie streaming (using http-streaming or long-polling). <br> 2698 | * 2699 | * @param {string, Object} Message to send. This is an object, string message is saved in data member. 2700 | * @private 2701 | */ 2702 | function _pushIE(message) { 2703 | if (_request.enableXDR && atmosphere.util.checkCORSSupport()) { 2704 | var rq = _getPushRequest(message); 2705 | // Do not reconnect since we are pushing. 2706 | rq.reconnect = false; 2707 | _jsonp(rq); 2708 | } else { 2709 | _pushAjaxMessage(message); 2710 | } 2711 | } 2712 | 2713 | /** 2714 | * Send a message using jsonp transport. <br> 2715 | * 2716 | * @param {string, Object} Message to send. This is an object, string message is saved in data member. 2717 | * @private 2718 | */ 2719 | function _pushJsonp(message) { 2720 | _pushAjaxMessage(message); 2721 | } 2722 | 2723 | function _getStringMessage(message) { 2724 | var msg = message; 2725 | if (typeof (msg) === 'object') { 2726 | msg = message.data; 2727 | } 2728 | return msg; 2729 | } 2730 | 2731 | /** 2732 | * Build request use to push message using method 'POST' <br>. Transport is defined as 'polling' and 'suspend' is set to false. 2733 | * 2734 | * @return {Object} Request object use to push message. 2735 | * @private 2736 | */ 2737 | function _getPushRequest(message) { 2738 | var msg = _getStringMessage(message); 2739 | 2740 | var rq = { 2741 | connected: false, 2742 | timeout: 60000, 2743 | method: 'POST', 2744 | url: _request.url, 2745 | contentType: _request.contentType, 2746 | headers: _request.headers, 2747 | reconnect: true, 2748 | callback: null, 2749 | data: msg, 2750 | suspend: false, 2751 | maxRequest: -1, 2752 | logLevel: 'info', 2753 | requestCount: 0, 2754 | withCredentials: _request.withCredentials, 2755 | transport: 'polling', 2756 | isOpen: true, 2757 | attachHeadersAsQueryString: true, 2758 | enableXDR: _request.enableXDR, 2759 | uuid: _request.uuid, 2760 | dispatchUrl: _request.dispatchUrl, 2761 | enableProtocol: false, 2762 | messageDelimiter: '|', 2763 | trackMessageLength: _request.trackMessageLength, 2764 | maxReconnectOnClose: _request.maxReconnectOnClose, 2765 | heartbeatTimer: _request.heartbeatTimer, 2766 | heartbeat: _request.heartbeat 2767 | }; 2768 | 2769 | if (typeof (message) === 'object') { 2770 | rq = atmosphere.util.extend(rq, message); 2771 | } 2772 | 2773 | return rq; 2774 | } 2775 | 2776 | /** 2777 | * Send a message using currently opened websocket. <br> 2778 | * 2779 | */ 2780 | function _pushWebSocket(message) { 2781 | var msg = atmosphere.util.isBinary(message) ? message : _getStringMessage(message); 2782 | var data; 2783 | try { 2784 | if (_request.dispatchUrl != null) { 2785 | data = _request.webSocketPathDelimiter + _request.dispatchUrl + _request.webSocketPathDelimiter + msg; 2786 | } else { 2787 | data = msg; 2788 | } 2789 | 2790 | if (!_websocket.canSendMessage) { 2791 | atmosphere.util.error("WebSocket not connected."); 2792 | return; 2793 | } 2794 | 2795 | _websocket.send(data); 2796 | 2797 | } catch (e) { 2798 | _websocket.onclose = function () { 2799 | }; 2800 | _clearState(); 2801 | 2802 | _reconnectWithFallbackTransport("Websocket failed. Downgrading to " + _request.fallbackTransport + " and resending " + message); 2803 | _pushAjaxMessage(message); 2804 | } 2805 | } 2806 | 2807 | function _localMessage(message) { 2808 | var m = JSON.parse(message); 2809 | if (m.id !== guid && typeof (_request.onLocalMessage) !== 'undefined') { 2810 | _request.onLocalMessage(m.event); 2811 | } 2812 | } 2813 | 2814 | function _prepareCallback(messageBody, state, errorCode, transport) { 2815 | _response.responseBody = messageBody; 2816 | _response.transport = transport; 2817 | _response.status = errorCode; 2818 | _response.state = state; 2819 | 2820 | _invokeCallback(); 2821 | } 2822 | 2823 | function _readHeaders(xdr, request) { 2824 | if (!request.readResponsesHeaders) { 2825 | if (!request.enableProtocol) { 2826 | request.uuid = guid; 2827 | } 2828 | } else { 2829 | try { 2830 | 2831 | var tempUUID = xdr.getResponseHeader('X-Atmosphere-tracking-id'); 2832 | if (tempUUID && tempUUID != null) { 2833 | request.uuid = tempUUID.split(" ").pop(); 2834 | } 2835 | } catch (e) { 2836 | } 2837 | } 2838 | } 2839 | 2840 | function _invokeFunction(response) { 2841 | _f(response, _request); 2842 | // Global 2843 | _f(response, atmosphere.util); 2844 | } 2845 | 2846 | function _f(response, f) { 2847 | switch (response.state) { 2848 | case "messageReceived": 2849 | _debug("Firing onMessage"); 2850 | _requestCount = 0; 2851 | if (typeof (f.onMessage) !== 'undefined') 2852 | f.onMessage(response); 2853 | 2854 | if (typeof (f.onmessage) !== 'undefined') 2855 | f.onmessage(response); 2856 | break; 2857 | case "error": 2858 | var dbgReasonPhrase = (typeof (response.reasonPhrase) != 'undefined') ? response.reasonPhrase : 'n/a'; 2859 | _debug("Firing onError, reasonPhrase: " + dbgReasonPhrase); 2860 | if (typeof (f.onError) !== 'undefined') 2861 | f.onError(response); 2862 | 2863 | if (typeof (f.onerror) !== 'undefined') 2864 | f.onerror(response); 2865 | break; 2866 | case "opening": 2867 | delete _request.closed; 2868 | _debug("Firing onOpen"); 2869 | if (typeof (f.onOpen) !== 'undefined') 2870 | f.onOpen(response); 2871 | 2872 | if (typeof (f.onopen) !== 'undefined') 2873 | f.onopen(response); 2874 | break; 2875 | case "messagePublished": 2876 | _debug("Firing messagePublished"); 2877 | if (typeof (f.onMessagePublished) !== 'undefined') 2878 | f.onMessagePublished(response); 2879 | break; 2880 | case "re-connecting": 2881 | _debug("Firing onReconnect"); 2882 | if (typeof (f.onReconnect) !== 'undefined') 2883 | f.onReconnect(_request, response); 2884 | break; 2885 | case "closedByClient": 2886 | _debug("Firing closedByClient"); 2887 | if (typeof (f.onClientTimeout) !== 'undefined') 2888 | f.onClientTimeout(_request); 2889 | break; 2890 | case "re-opening": 2891 | delete _request.closed; 2892 | _debug("Firing onReopen"); 2893 | if (typeof (f.onReopen) !== 'undefined') 2894 | f.onReopen(_request, response); 2895 | break; 2896 | case "fail-to-reconnect": 2897 | _debug("Firing onFailureToReconnect"); 2898 | if (typeof (f.onFailureToReconnect) !== 'undefined') 2899 | f.onFailureToReconnect(_request, response); 2900 | break; 2901 | case "unsubscribe": 2902 | case "closed": 2903 | var closed = typeof (_request.closed) !== 'undefined' ? _request.closed : false; 2904 | 2905 | if (!closed) { 2906 | _debug("Firing onClose (" + response.state + " case)"); 2907 | if (typeof (f.onClose) !== 'undefined') { 2908 | f.onClose(response); 2909 | } 2910 | 2911 | if (typeof (f.onclose) !== 'undefined') { 2912 | f.onclose(response); 2913 | } 2914 | } else { 2915 | _debug("Request already closed, not firing onClose (" + response.state + " case)"); 2916 | } 2917 | _request.closed = true; 2918 | break; 2919 | case "openAfterResume": 2920 | if (typeof (f.onOpenAfterResume) !== 'undefined') 2921 | f.onOpenAfterResume(_request); 2922 | break; 2923 | } 2924 | } 2925 | 2926 | function _invokeClose(wasOpen) { 2927 | if (_response.state !== 'closed') { 2928 | _response.state = 'closed'; 2929 | _response.responseBody = ""; 2930 | _response.messages = []; 2931 | _response.status = !wasOpen ? 501 : 200; 2932 | _invokeCallback(); 2933 | } 2934 | } 2935 | 2936 | /** 2937 | * Invoke request callbacks. 2938 | * 2939 | * @private 2940 | */ 2941 | function _invokeCallback() { 2942 | var call = function (index, func) { 2943 | func(_response); 2944 | }; 2945 | 2946 | if (_localStorageService == null && _localSocketF != null) { 2947 | _localSocketF(_response.responseBody); 2948 | } 2949 | 2950 | _request.reconnect = _request.mrequest; 2951 | 2952 | var isString = typeof (_response.responseBody) === 'string'; 2953 | var messages = (isString && _request.trackMessageLength) ? (_response.messages.length > 0 ? _response.messages : ['']) : new Array( 2954 | _response.responseBody); 2955 | for (var i = 0; i < messages.length; i++) { 2956 | 2957 | if (messages.length > 1 && messages[i].length === 0) { 2958 | continue; 2959 | } 2960 | _response.responseBody = (isString) ? atmosphere.util.trim(messages[i]) : messages[i]; 2961 | 2962 | if (_localStorageService == null && _localSocketF != null) { 2963 | _localSocketF(_response.responseBody); 2964 | } 2965 | 2966 | if (_response.state === "messageReceived") { 2967 | if (_response.responseBody.length === 0) { 2968 | continue; 2969 | } else if (isString && _heartbeatPadding === _response.responseBody) { 2970 | // reset the internal reconnect counter, when we received also heartbeat message from server 2971 | _requestCount = 0; 2972 | continue; 2973 | } 2974 | } else if (_response.state === "opening" || _response.state === "re-opening") { 2975 | // reset the internal reconnect counter when the connection is opened/reopened 2976 | _requestCount = 0; 2977 | } 2978 | 2979 | _invokeFunction(_response); 2980 | 2981 | // Invoke global callbacks 2982 | if (callbacks.length > 0) { 2983 | if (_canLog('debug')) { 2984 | atmosphere.util.debug("Invoking " + callbacks.length + " global callbacks: " + _response.state); 2985 | } 2986 | try { 2987 | atmosphere.util.each(callbacks, call); 2988 | } catch (e) { 2989 | atmosphere.util.log(_request.logLevel, ["Callback exception" + e]); 2990 | } 2991 | } 2992 | 2993 | // Invoke request callback 2994 | if (typeof (_request.callback) === 'function') { 2995 | if (_canLog('debug')) { 2996 | atmosphere.util.debug("Invoking request callbacks"); 2997 | } 2998 | try { 2999 | _request.callback(_response); 3000 | } catch (e) { 3001 | atmosphere.util.log(_request.logLevel, ["Callback exception" + e]); 3002 | } 3003 | } 3004 | } 3005 | } 3006 | 3007 | this.subscribe = function (options) { 3008 | _subscribe(options); 3009 | _execute(); 3010 | }; 3011 | 3012 | this.execute = function () { 3013 | _execute(); 3014 | }; 3015 | 3016 | this.close = function () { 3017 | _close(); 3018 | }; 3019 | 3020 | this.disconnect = function () { 3021 | _disconnect(); 3022 | }; 3023 | 3024 | this.getUrl = function () { 3025 | return _request.url; 3026 | }; 3027 | 3028 | this.push = function (message, dispatchUrl) { 3029 | if (dispatchUrl != null) { 3030 | var originalDispatchUrl = _request.dispatchUrl; 3031 | _request.dispatchUrl = dispatchUrl; 3032 | _push(message, _request); 3033 | _request.dispatchUrl = originalDispatchUrl; 3034 | } else { 3035 | _push(message, _request); 3036 | } 3037 | }; 3038 | 3039 | this.getUUID = function () { 3040 | return _request.uuid; 3041 | }; 3042 | 3043 | this.pushLocal = function (message) { 3044 | _intraPush(message); 3045 | }; 3046 | 3047 | this.enableProtocol = function () { 3048 | return _request.enableProtocol; 3049 | }; 3050 | 3051 | this.init = function () { 3052 | _init(); 3053 | }; 3054 | 3055 | this.request = _request; 3056 | this.response = _response; 3057 | } 3058 | }; 3059 | 3060 | atmosphere.subscribe = function (url, callback, request) { 3061 | if (typeof (callback) === 'function') { 3062 | atmosphere.addCallback(callback); 3063 | } 3064 | 3065 | if (typeof (url) !== "string") { 3066 | request = url; 3067 | } else { 3068 | request.url = url; 3069 | } 3070 | 3071 | // https://github.com/Atmosphere/atmosphere-javascript/issues/58 3072 | uuid = ((typeof (request) !== 'undefined') && typeof (request.uuid) !== 'undefined') ? request.uuid : 0; 3073 | 3074 | var rq = new atmosphere.AtmosphereRequest(request); 3075 | rq.execute(); 3076 | 3077 | requests[requests.length] = rq; 3078 | return rq; 3079 | }; 3080 | 3081 | atmosphere.unsubscribe = function () { 3082 | if (requests.length > 0) { 3083 | var requestsClone = [].concat(requests); 3084 | for (var i = 0; i < requestsClone.length; i++) { 3085 | var rq = requestsClone[i]; 3086 | rq.close(); 3087 | clearTimeout(rq.response.request.id); 3088 | 3089 | if (rq.heartbeatTimer) { 3090 | clearTimeout(rq.heartbeatTimer); 3091 | } 3092 | } 3093 | } 3094 | requests = []; 3095 | callbacks = []; 3096 | }; 3097 | 3098 | atmosphere.unsubscribeUrl = function (url) { 3099 | var idx = -1; 3100 | if (requests.length > 0) { 3101 | for (var i = 0; i < requests.length; i++) { 3102 | var rq = requests[i]; 3103 | 3104 | // Suppose you can subscribe once to an url 3105 | if (rq.getUrl() === url) { 3106 | rq.close(); 3107 | clearTimeout(rq.response.request.id); 3108 | 3109 | if (rq.heartbeatTimer) { 3110 | clearTimeout(rq.heartbeatTimer); 3111 | } 3112 | 3113 | idx = i; 3114 | break; 3115 | } 3116 | } 3117 | } 3118 | if (idx >= 0) { 3119 | requests.splice(idx, 1); 3120 | } 3121 | }; 3122 | 3123 | atmosphere.addCallback = function (func) { 3124 | if (atmosphere.util.inArray(func, callbacks) === -1) { 3125 | callbacks.push(func); 3126 | } 3127 | }; 3128 | 3129 | atmosphere.removeCallback = function (func) { 3130 | var index = atmosphere.util.inArray(func, callbacks); 3131 | if (index !== -1) { 3132 | callbacks.splice(index, 1); 3133 | } 3134 | }; 3135 | 3136 | atmosphere.util = { 3137 | browser: {}, 3138 | 3139 | parseHeaders: function (headerString) { 3140 | var match, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, headers = {}; 3141 | while (match = rheaders.exec(headerString)) { 3142 | headers[match[1]] = match[2]; 3143 | } 3144 | return headers; 3145 | }, 3146 | 3147 | now: function () { 3148 | return new Date().getTime(); 3149 | }, 3150 | 3151 | isArray: function (array) { 3152 | return Object.prototype.toString.call(array) === "[object Array]"; 3153 | }, 3154 | 3155 | inArray: function (elem, array) { 3156 | if (!Array.prototype.indexOf) { 3157 | var len = array.length; 3158 | for (var i = 0; i < len; ++i) { 3159 | if (array[i] === elem) { 3160 | return i; 3161 | } 3162 | } 3163 | return -1; 3164 | } 3165 | return array.indexOf(elem); 3166 | }, 3167 | 3168 | isBinary: function (data) { 3169 | // True if data is an instance of Blob, ArrayBuffer or ArrayBufferView 3170 | return /^\[object\s(?:Blob|ArrayBuffer|.+Array)\]$/.test(Object.prototype.toString.call(data)); 3171 | }, 3172 | 3173 | isFunction: function (fn) { 3174 | return Object.prototype.toString.call(fn) === "[object Function]"; 3175 | }, 3176 | 3177 | getAbsoluteURL: function (url) { 3178 | if (typeof (document.createElement) === 'undefined') { 3179 | // assuming the url to be already absolute when DOM is not supported 3180 | return url; 3181 | } 3182 | var div = document.createElement("div"); 3183 | 3184 | // Uses an innerHTML property to obtain an absolute URL 3185 | div.innerHTML = '<a href="' + url + '"></a>'; 3186 | 3187 | // encodeURI and decodeURI are needed to normalize URL between IE and non-IE, 3188 | // since IE doesn't encode the href property value and return it - http://jsfiddle.net/Yq9M8/1/ 3189 | 3190 | var ua = window.navigator.userAgent; 3191 | if (ua.indexOf('MSIE ') > 0 || ua.indexOf('Trident/') > 0 || ua.indexOf('Edge/') > 0) { 3192 | return atmosphere.util.fixedEncodeURI(decodeURI(div.firstChild.href)); 3193 | } 3194 | return div.firstChild.href; 3195 | }, 3196 | 3197 | fixedEncodeURI: function (str) { 3198 | return encodeURI(str).replace(/%5B/g, '[').replace(/%5D/g, ']'); 3199 | }, 3200 | 3201 | prepareURL: function (url) { 3202 | // Attaches a time stamp to prevent caching 3203 | var ts = atmosphere.util.now(); 3204 | var ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts); 3205 | 3206 | return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : ""); 3207 | }, 3208 | 3209 | trim: function (str) { 3210 | if (!String.prototype.trim) { 3211 | return str.toString().replace(/(?:(?:^|\n)\s+|\s+(?:$|\n))/g, "").replace(/\s+/g, " "); 3212 | } else { 3213 | return str.toString().trim(); 3214 | } 3215 | }, 3216 | 3217 | param: function (params) { 3218 | var prefix, s = []; 3219 | 3220 | function add(key, value) { 3221 | value = atmosphere.util.isFunction(value) ? value() : (value == null ? "" : value); 3222 | s.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)); 3223 | } 3224 | 3225 | function buildParams(prefix, obj) { 3226 | var name; 3227 | 3228 | if (atmosphere.util.isArray(obj)) { 3229 | atmosphere.util.each(obj, function (i, v) { 3230 | if (/\[\]$/.test(prefix)) { 3231 | add(prefix, v); 3232 | } else { 3233 | buildParams(prefix + "[" + (typeof v === "object" ? i : "") + "]", v); 3234 | } 3235 | }); 3236 | } else if (Object.prototype.toString.call(obj) === "[object Object]") { 3237 | for (name in obj) { 3238 | buildParams(prefix + "[" + name + "]", obj[name]); 3239 | } 3240 | } else { 3241 | add(prefix, obj); 3242 | } 3243 | } 3244 | 3245 | for (prefix in params) { 3246 | buildParams(prefix, params[prefix]); 3247 | } 3248 | 3249 | return s.join("&").replace(/%20/g, "+"); 3250 | }, 3251 | 3252 | storage: (function () { 3253 | try { 3254 | return !!(window.localStorage && window.StorageEvent); 3255 | } catch (e) { 3256 | //Firefox throws an exception here, see 3257 | //https://bugzilla.mozilla.org/show_bug.cgi?id=748620 3258 | return false; 3259 | } 3260 | })(), 3261 | 3262 | iterate: function (fn, interval) { 3263 | var timeoutId; 3264 | 3265 | // Though the interval is 0 for real-time application, there is a delay between setTimeout calls 3266 | // For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting 3267 | interval = interval || 0; 3268 | 3269 | (function loop() { 3270 | timeoutId = setTimeout(function () { 3271 | if (fn() === false) { 3272 | return; 3273 | } 3274 | 3275 | loop(); 3276 | }, interval); 3277 | })(); 3278 | 3279 | return function () { 3280 | clearTimeout(timeoutId); 3281 | }; 3282 | }, 3283 | 3284 | each: function (obj, callback) { 3285 | if (!obj) return; 3286 | var value, i = 0, length = obj.length, isArray = atmosphere.util.isArray(obj); 3287 | 3288 | if (isArray) { 3289 | for (; i < length; i++) { 3290 | value = callback.call(obj[i], i, obj[i]); 3291 | 3292 | if (value === false) { 3293 | break; 3294 | } 3295 | } 3296 | } else { 3297 | for (i in obj) { 3298 | value = callback.call(obj[i], i, obj[i]); 3299 | 3300 | if (value === false) { 3301 | break; 3302 | } 3303 | } 3304 | } 3305 | 3306 | return obj; 3307 | }, 3308 | 3309 | extend: function (target) { 3310 | var i, options, name; 3311 | 3312 | for (i = 1; i < arguments.length; i++) { 3313 | if ((options = arguments[i]) != null) { 3314 | for (name in options) { 3315 | target[name] = options[name]; 3316 | } 3317 | } 3318 | } 3319 | 3320 | return target; 3321 | }, 3322 | on: function (elem, type, fn) { 3323 | if (elem.addEventListener) { 3324 | elem.addEventListener(type, fn, false); 3325 | } else if (elem.attachEvent) { 3326 | elem.attachEvent("on" + type, fn); 3327 | } 3328 | }, 3329 | off: function (elem, type, fn) { 3330 | if (elem.removeEventListener) { 3331 | elem.removeEventListener(type, fn, false); 3332 | } else if (elem.detachEvent) { 3333 | elem.detachEvent("on" + type, fn); 3334 | } 3335 | }, 3336 | 3337 | log: function (level, args) { 3338 | if (window.console) { 3339 | var logger = window.console[level]; 3340 | if (typeof logger === 'function') { 3341 | logger.apply(window.console, args); 3342 | } 3343 | } 3344 | }, 3345 | 3346 | warn: function () { 3347 | atmosphere.util.log('warn', arguments); 3348 | }, 3349 | 3350 | info: function () { 3351 | atmosphere.util.log('info', arguments); 3352 | }, 3353 | 3354 | debug: function () { 3355 | atmosphere.util.log('debug', arguments); 3356 | }, 3357 | 3358 | error: function () { 3359 | atmosphere.util.log('error', arguments); 3360 | }, 3361 | 3362 | xhr: function () { 3363 | try { 3364 | return new window.XMLHttpRequest(); 3365 | } catch (e1) { 3366 | try { 3367 | return new window.ActiveXObject("Microsoft.XMLHTTP"); 3368 | } catch (e2) { 3369 | } 3370 | } 3371 | }, 3372 | 3373 | checkCORSSupport: function () { 3374 | if (atmosphere.util.browser.msie && !window.XDomainRequest && +atmosphere.util.browser.version.split(".")[0] < 11) { 3375 | return true; 3376 | } else if (atmosphere.util.browser.opera && +atmosphere.util.browser.version.split(".") < 12.0) { 3377 | return true; 3378 | } 3379 | 3380 | // KreaTV 4.1 -> 4.4 3381 | else if (atmosphere.util.trim(navigator.userAgent).slice(0, 16) === "KreaTVWebKit/531") { 3382 | return true; 3383 | } 3384 | // KreaTV 3.8 3385 | else if (atmosphere.util.trim(navigator.userAgent).slice(-7).toLowerCase() === "kreatel") { 3386 | return true; 3387 | } 3388 | 3389 | // Force older Android versions to use CORS as some version like 2.2.3 fail otherwise 3390 | var ua = navigator.userAgent.toLowerCase(); 3391 | var androidVersionMatches = ua.match(/.+android ([0-9]{1,2})/i), 3392 | majorVersion = parseInt((androidVersionMatches && androidVersionMatches[0]) || -1, 10); 3393 | if (!isNaN(majorVersion) && majorVersion > -1 && majorVersion < 3) { 3394 | return true; 3395 | } 3396 | return false; 3397 | } 3398 | }; 3399 | 3400 | // Browser sniffing 3401 | (function () { 3402 | var ua = navigator.userAgent.toLowerCase(), 3403 | match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 3404 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 3405 | /(msie) ([\w.]+)/.exec(ua) || 3406 | /(trident)(?:.*? rv:([\w.]+)|)/.exec(ua) || 3407 | ua.indexOf("android") < 0 && /version\/(.+) (safari)/.exec(ua) || 3408 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 3409 | []; 3410 | 3411 | // Swaps variables 3412 | if (match[2] === "safari") { 3413 | match[2] = match[1]; 3414 | match[1] = "safari"; 3415 | } 3416 | atmosphere.util.browser[match[1] || ""] = true; 3417 | atmosphere.util.browser.version = match[2] || "0"; 3418 | atmosphere.util.browser.vmajor = atmosphere.util.browser.version.split(".")[0]; 3419 | 3420 | // Trident is the layout engine of the Internet Explorer 3421 | // IE 11 has no "MSIE: 11.0" token 3422 | if (atmosphere.util.browser.trident) { 3423 | atmosphere.util.browser.msie = true; 3424 | } 3425 | 3426 | // The storage event of Internet Explorer and Firefox 3 works strangely 3427 | if (atmosphere.util.browser.msie || (atmosphere.util.browser.mozilla && +atmosphere.util.browser.version.split(".")[0] === 1)) { 3428 | atmosphere.util.storage = false; 3429 | } 3430 | })(); 3431 | 3432 | atmosphere.callbacks = { 3433 | unload: function () { 3434 | atmosphere.util.debug(new Date() + " Atmosphere: " + "unload event"); 3435 | // Check if we should use the old unload behavior or if beforeunload hasn't handled cleanup 3436 | var shouldCleanupInUnload = requests.length > 0 && 3437 | (requests[0].request.useBeforeUnloadForCleanup === false || !_beforeUnloadState); 3438 | 3439 | if (shouldCleanupInUnload) { 3440 | atmosphere.unsubscribe(); 3441 | } 3442 | }, 3443 | beforeUnload: function () { 3444 | atmosphere.util.debug(new Date() + " Atmosphere: " + "beforeunload event"); 3445 | 3446 | // If another unload attempt was made within the 5000ms timeout 3447 | if (atmosphere._beforeUnloadTimeoutId != null) { 3448 | clearTimeout(atmosphere._beforeUnloadTimeoutId); 3449 | } 3450 | 3451 | // ATMOSPHERE-JAVASCRIPT-143: Delay reconnect to avoid reconnect attempts before an actual unload (we don't know if an unload will happen, yet) 3452 | _beforeUnloadState = true; 3453 | 3454 | // Check if we should cleanup in beforeunload (default behavior for better bfcache compatibility) 3455 | var shouldCleanupInBeforeUnload = requests.length > 0 && 3456 | requests[0].request.useBeforeUnloadForCleanup !== false; 3457 | 3458 | if (shouldCleanupInBeforeUnload) { 3459 | // Primary cleanup now happens here instead of in unload event 3460 | // This ensures compatibility with Chrome's bfcache and follows modern best practices 3461 | atmosphere.unsubscribe(); 3462 | } 3463 | 3464 | atmosphere._beforeUnloadTimeoutId = setTimeout(function () { 3465 | atmosphere.util.debug(new Date() + " Atmosphere: " + "beforeunload event timeout reached. Reset _beforeUnloadState flag"); 3466 | _beforeUnloadState = false; 3467 | }, 5000); 3468 | }, 3469 | offline: function () { 3470 | atmosphere.util.debug(new Date() + " Atmosphere: offline event"); 3471 | offline = true; 3472 | if (requests.length > 0) { 3473 | var requestsClone = [].concat(requests); 3474 | for (var i = 0; i < requestsClone.length; i++) { 3475 | var rq = requestsClone[i]; 3476 | if (rq.request.handleOnlineOffline) { 3477 | rq.close(); 3478 | clearTimeout(rq.response.request.id); 3479 | 3480 | if (rq.heartbeatTimer) { 3481 | clearTimeout(rq.heartbeatTimer); 3482 | } 3483 | } 3484 | } 3485 | } 3486 | }, 3487 | online: function () { 3488 | atmosphere.util.debug(new Date() + " Atmosphere: online event"); 3489 | if (requests.length > 0) { 3490 | for (var i = 0; i < requests.length; i++) { 3491 | if (requests[i].request.handleOnlineOffline) { 3492 | requests[i].init(); 3493 | requests[i].execute(); 3494 | } 3495 | } 3496 | } 3497 | offline = false; 3498 | } 3499 | }; 3500 | 3501 | atmosphere.bindEvents = function () { 3502 | atmosphere.util.on(window, "unload", atmosphere.callbacks.unload); 3503 | atmosphere.util.on(window, "beforeunload", atmosphere.callbacks.beforeUnload); 3504 | atmosphere.util.on(window, "offline", atmosphere.callbacks.offline); 3505 | atmosphere.util.on(window, "online", atmosphere.callbacks.online); 3506 | }; 3507 | 3508 | atmosphere.unbindEvents = function () { 3509 | atmosphere.util.off(window, "unload", atmosphere.callbacks.unload); 3510 | atmosphere.util.off(window, "beforeunload", atmosphere.callbacks.beforeUnload); 3511 | atmosphere.util.off(window, "offline", atmosphere.callbacks.offline); 3512 | atmosphere.util.off(window, "online", atmosphere.callbacks.online); 3513 | }; 3514 | 3515 | atmosphere.bindEvents(); 3516 | 3517 | return atmosphere; 3518 | })); 3519 | /* jshint eqnull:true, noarg:true, noempty:true, eqeqeq:true, evil:true, laxbreak:true, undef:true, browser:true, indent:false, maxerr:50 */ 3520 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 3 | <parent> 4 | <groupId>org.sonatype.oss</groupId> 5 | <artifactId>oss-parent</artifactId> 6 | <version>9</version> 7 | </parent> 8 | <modelVersion>4.0.0</modelVersion> 9 | <groupId>org.atmosphere</groupId> 10 | <artifactId>javascript-project</artifactId> 11 | <name>javascript-project</name> 12 | <version>4.0.2-SNAPSHOT</version> 13 | <packaging>pom</packaging> 14 | <description> 15 | Javascript clients for Atmosphere 16 | </description> 17 | <url>http://github.com/Atmosphere/atmosphere-javascript</url> 18 | <scm> 19 | <connection>scm:git:git@github.com:Atmosphere/atmosphere-javascript.git</connection> 20 | <developerConnection>scm:git:git@github.com:Atmosphere/atmosphere-javascript.git</developerConnection> 21 | <url>http://github.com/Atmosphere/atmosphere-javascript</url> 22 | <tag>HEAD</tag> 23 | </scm> 24 | <prerequisites> 25 | <maven>2.2.0</maven> 26 | </prerequisites> 27 | <developers> 28 | <developer> 29 | <id>jfarcand</id> 30 | <name>Jeanfrancois Arcand</name> 31 | <email>jfarcand@apache.org</email> 32 | </developer> 33 | </developers> 34 | <licenses> 35 | <license> 36 | <name>Apache License 2.0</name> 37 | <url>http://www.apache.org/licenses/LICENSE-2.0.html</url> 38 | <distribution>repo</distribution> 39 | </license> 40 | </licenses> 41 | <dependencyManagement> 42 | <dependencies> 43 | </dependencies> 44 | </dependencyManagement> 45 | <build> 46 | <extensions> 47 | <!-- Enabling the use of SSH --> 48 | <extension> 49 | <groupId>org.apache.maven.wagon</groupId> 50 | <artifactId>wagon-ssh-external</artifactId> 51 | <version>3.4.0</version> 52 | </extension> 53 | </extensions> 54 | <defaultGoal>install</defaultGoal> 55 | <plugins> 56 | <plugin> 57 | <groupId>org.apache.maven.plugins</groupId> 58 | <artifactId>maven-resources-plugin</artifactId> 59 | <version>2.4.3</version> 60 | <configuration> 61 | <encoding>UTF-8</encoding> 62 | </configuration> 63 | </plugin> 64 | <plugin> 65 | <groupId>org.apache.maven.plugins</groupId> 66 | <artifactId>maven-release-plugin</artifactId> 67 | <version>3.0.0-M1</version> 68 | </plugin> 69 | <plugin> 70 | <artifactId>maven-clean-plugin</artifactId> 71 | <version>2.4</version> 72 | <configuration> 73 | <filesets> 74 | <fileset> 75 | <directory>${project.basedir}/META-INF</directory> 76 | </fileset> 77 | <fileset> 78 | <directory>${project.basedir}/works</directory> 79 | </fileset> 80 | <fileset> 81 | <directory>${project.basedir}/overlays</directory> 82 | </fileset> 83 | <fileset> 84 | <directory>${project.basedir}</directory> 85 | <includes> 86 | <include>*.log</include> 87 | </includes> 88 | </fileset> 89 | </filesets> 90 | </configuration> 91 | </plugin> 92 | <plugin> 93 | <groupId>org.apache.maven.plugins</groupId> 94 | <artifactId>maven-source-plugin</artifactId> 95 | <version>2.1.2</version> 96 | <executions> 97 | <execution> 98 | <id>attach-sources</id> 99 | <phase>verify</phase> 100 | <goals> 101 | <goal>jar-no-fork</goal> 102 | </goals> 103 | </execution> 104 | </executions> 105 | </plugin> 106 | <plugin> 107 | <groupId>org.apache.maven.plugins</groupId> 108 | <artifactId>maven-jxr-plugin</artifactId> 109 | <version>2.3</version> 110 | <configuration> 111 | <aggregate>true</aggregate> 112 | </configuration> 113 | </plugin> 114 | </plugins> 115 | </build> 116 | <modules> 117 | <module>modules/javascript</module> 118 | </modules> 119 | <reporting> 120 | <outputDirectory>target/site</outputDirectory> 121 | <plugins> 122 | <plugin> 123 | <groupId>org.apache.maven.plugins</groupId> 124 | <artifactId>maven-javadoc-plugin</artifactId> 125 | <configuration> 126 | <aggregate>true</aggregate> 127 | <debug>true</debug> 128 | <links> 129 | <link>http://docs.oracle.com/javaee/6/docs/api/</link> 130 | <link>http://docs.oracle.com/javase/6/docs/api/</link> 131 | </links> 132 | </configuration> 133 | </plugin> 134 | <plugin> 135 | <groupId>org.apache.maven.plugins</groupId> 136 | <artifactId>maven-jxr-plugin</artifactId> 137 | <version>2.3</version> 138 | <configuration> 139 | <aggregate>true</aggregate> 140 | </configuration> 141 | </plugin> 142 | <plugin> 143 | <groupId>org.apache.maven.plugins</groupId> 144 | <artifactId>maven-project-info-reports-plugin</artifactId> 145 | <reportSets> 146 | <reportSet> 147 | <reports> 148 | <report>project-team</report> 149 | </reports> 150 | </reportSet> 151 | </reportSets> 152 | </plugin> 153 | </plugins> 154 | </reporting> 155 | <properties> 156 | <distMgmtSnapshotsUrl>https://oss.sonatype.org/content/repositories/snapshots</distMgmtSnapshotsUrl> 157 | <surefire.redirectTestOutputToFile>false</surefire.redirectTestOutputToFile> 158 | </properties> 159 | </project> 160 | 161 | 162 | 163 | --------------------------------------------------------------------------------