├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Changelog.md ├── Makefile ├── README.md ├── XMLHttpRequest.js ├── closure-terminate-fixes-r2519.patch ├── dist ├── bcsocket-uncompressed.js ├── bcsocket.js ├── node-bcsocket-uncompressed.js ├── node-bcsocket.js └── server.js ├── docs ├── docco.css ├── server.html └── test.html ├── examples ├── chatserver.coffee └── package.json ├── index.js ├── lib ├── README.md ├── bcsocket.coffee ├── browserchannel.coffee ├── debug.coffee ├── handler-externs.js ├── nodejs-override.coffee └── server.coffee ├── package.json └── test ├── bcsocket.coffee ├── mocha.opts ├── runserver.coffee ├── server.coffee └── web ├── assert.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | node_modules 4 | tmp 5 | closure-library 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: "npm install" 2 | script: "npm test" 3 | language: node_js 4 | node_js: 5 | - "0.10" 6 | - "0.12" 7 | - "iojs" 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to my project 2 | 3 | **tldr;** 4 | 5 | - Email me if I forget about you 6 | - Write unit tests 7 | - Make small pull requests 8 | - If you need to recompile, commit those changes separately 9 | 10 | I love pull requests, but I'm sometimes bad at replying to pull requests & 11 | emails. Sorry - its not you, its me. Please email me regularly if replying is 12 | important. (I'm me@josephg.com ) 13 | 14 | I get mad if I have to fix bugs in your code later. 15 | As such, all new features introduced in pull requests MUST include tests 16 | for any added functionality. Unless they fix an active bug, I will not accept a 17 | pull request without tests. If they fix an active bug and writing tests sounds 18 | hard, I might add tests myself. 19 | 20 | Please break pull requests up into small atomic changes. Small changes are easy 21 | to evaluate and merge. Large changes are much harder. 22 | 23 | Please separate recompilation commits from actual code changes - recompiling 24 | can generate giant diffs and make pull requests hard to read. 25 | 26 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | > This document was only added recently. Its missing a lot of the history. 2 | 3 | # 1.2.0 -> 2.0.0 4 | 5 | The client API has better websocket API support. 6 | 7 | - **IMPORTANT API CHANGE** `client.onmessage` now receives an object with a data 8 | property, instead of the message raw. This better matches the websocket API. 9 | - I've added a couple of capability flags to the session object, namely 10 | `canSendWhileConnecting` and `canSendJSON`. 11 | - Fixed a bug where options.prev had no effect. 12 | - The client no longer throws if you send messages after the socket is closed. 13 | It silently drops them instead (for better or worse...) 14 | 15 | If your code previously looked like this: 16 | 17 | ```javascript 18 | session.onmessage = function(data) { 19 | window.alert(data.x, data.y); 20 | }; 21 | ``` 22 | 23 | it will now look like this: 24 | 25 | ```javascript 26 | session.onmessage = function(message) { 27 | window.alert(message.data.x, message.data.y); 28 | }; 29 | ``` 30 | 31 | Despite the major version bump, this release actually has very few changes. I 32 | was considering rewriting all of browserchannel in javascript (I'd still like to 33 | at some point), but I worry that doing so will introduce new bugs in the code 34 | that weren't there before. The version bump is necessary because of the change 35 | to the client API. 36 | 37 | I almost committed a [closure-javascript version of 38 | BCSocket](https://github.com/josephg/node-browserchannel/blob/javascript- 39 | bcsocket/lib/bcsocket.js) but it has new bugs, its bigger, its harder to read 40 | and even with better closure compiler type annotations it compiles to more code. 41 | I decided the transition isn't worth it at the moment. If/when I make that 42 | change, it won't result in any API changes so I can do it without bumping the 43 | major version. 44 | 45 | I also almost changed the server API around connections. I may still add a 46 | .stream() method to server sessions, but I haven't for now because node's 47 | Writable streams are unusable by browserchannel. I could expose the old node 48 | streams 1 duplex API, but I decided instead to just keep the API we have now. 49 | (its *fine*). 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # I require a local version of the closure library because the version in google's 2 | # REST service is too old and crufty to be useful. 3 | # 4 | # You can get the closure library like this: 5 | # 6 | # % git clone https://github.com/google/closure-library 7 | # 8 | # And download the closure compiler JAR here: 9 | # 10 | # http://dl.google.com/closure-compiler/compiler-latest.zip 11 | # 12 | # I also compile with a patch to the closure library (which is in the root of 13 | # this repo). The patch fixes some cleanup issues in the closure library to 14 | # make sure everything gets cleaned up properly when connections close. 15 | 16 | .PHONY: clean, all, test, check-java 17 | 18 | CLOSURE_DIR = ../closure-library 19 | CLOSURE_COMPILER = ../closure-library/compiler.jar 20 | 21 | CLOSURE_BUILDER = $(CLOSURE_DIR)/closure/bin/build/closurebuilder.py 22 | 23 | CLOSURE_CFLAGS = \ 24 | --root="$(CLOSURE_DIR)" \ 25 | --root=tmp/ \ 26 | --output_mode=compiled \ 27 | --compiler_jar="$(CLOSURE_COMPILER)" \ 28 | --compiler_flags=--compilation_level=ADVANCED_OPTIMIZATIONS \ 29 | --compiler_flags=--warning_level=DEFAULT \ 30 | --compiler_flags=--externs=lib/handler-externs.js \ 31 | --namespace=bc.BCSocket 32 | 33 | PRETTY_PRINT = --compiler_flags=--formatting=PRETTY_PRINT 34 | 35 | COFFEE = ./node_modules/.bin/coffee 36 | MOCHA = ./node_modules/.bin/mocha 37 | 38 | all: dist/server.js dist/bcsocket.js dist/node-bcsocket.js dist/bcsocket-uncompressed.js dist/node-bcsocket-uncompressed.js 39 | 40 | clean: 41 | rm -rf tmp 42 | 43 | test: 44 | $(MOCHA) 45 | 46 | tmp/%.js: lib/%.coffee 47 | $(COFFEE) -bco tmp $+ 48 | 49 | dist/%.js: tmp/compiled-%.js 50 | echo '(function(){' > $@ 51 | cat $+ >> $@ 52 | echo "})();" >> $@ 53 | 54 | # The server should be in dist/ too, but we don't need to compile that with closure. 55 | dist/server.js: lib/server.coffee 56 | $(COFFEE) -bco dist $< 57 | 58 | ## 59 | # Things can fail silently with the wrong java version. 60 | # 61 | # While jscompiler.py does parse the currently installed java version 62 | # and is supposed to abort on Java < 1.7, double checking here 63 | # adds safety by aborting the entire build. 64 | # 65 | # Additionally, with JDK 8 expected next month, this provides 66 | # insurance until we're sure that 1.8 won't break anything. 67 | # 68 | # At this time closure/bin/build/jscompiler.py uses whatever java 69 | # is currently in the path, and does not read $JAVA_HOME 70 | check-java: 71 | java -version 2>&1 | grep -e "[^\d\.]1\.7" 72 | 73 | tmp/compiled-bcsocket.js: check-java tmp/bcsocket.js tmp/browserchannel.js 74 | $(CLOSURE_BUILDER) $(CLOSURE_CFLAGS) > $@ 75 | 76 | tmp/compiled-node-bcsocket.js: check-java tmp/bcsocket.js tmp/nodejs-override.js tmp/browserchannel.js 77 | $(CLOSURE_BUILDER) $(CLOSURE_CFLAGS) --namespace=bc.node > $@ 78 | 79 | tmp/compiled-bcsocket-uncompressed.js: check-java tmp/bcsocket.js tmp/browserchannel.js 80 | $(CLOSURE_BUILDER) $(CLOSURE_CFLAGS) --compiler_flags=--formatting=PRETTY_PRINT > $@ 81 | 82 | tmp/compiled-node-bcsocket-uncompressed.js: check-java tmp/bcsocket.js tmp/nodejs-override.js tmp/browserchannel.js 83 | $(CLOSURE_BUILDER) $(CLOSURE_CFLAGS) --compiler_flags=--formatting=PRETTY_PRINT --namespace=bc.node > $@ 84 | 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A [BrowserChannel](http://closure-library.googlecode.com/svn/trunk/closure/goog/net/browserchannel.js) server. 2 | 3 | [![Build Status](https://travis-ci.org/josephg/node-browserchannel.svg?branch=master)](https://travis-ci.org/josephg/node-browserchannel) 4 | 5 | **tldr;** Its like socket.io, but it scales better and it has fewer bugs. It 6 | just does long polling. It also doesn't support websockets and doesn't support 7 | cross-domain requests out of the box. 8 | 9 | > *Note:* [Websocket support](http://caniuse.com/#feat=websockets) is now reasonably universal. Strongly consider using raw websockets for new projects. 10 | 11 | BrowserChannel is google's version of [socket.io](http://socket.io) from when they first put 12 | chat in gmail. Unlike socket.io, browserchannel guarantees: 13 | 14 | - Messages will arrive in order 15 | - Messages will never arrive on the server after a connection has closed 16 | - The mail will always get through on any browser that google talk works on, which is all of them. 17 | 18 | node-browserchannel: 19 | 20 | - Is compatible with the closure library's browserchannel implementation 21 | - Is super thoroughly tested 22 | - Works in IE5.5+, iOS, Safari, Chrome, Firefox, etc. 23 | - Works in any network environment (incl. behind buffering proxies) 24 | 25 | --- 26 | 27 | # Use it 28 | 29 | # npm install browserchannel 30 | 31 | Browserchannel is implemented as connect middleware. Here's an echo server: 32 | 33 | ```javascript 34 | var browserChannel = require('browserchannel').server; 35 | var connect = require('connect'); 36 | 37 | var server = connect( 38 | connect.static("#{__dirname}/public"), 39 | browserChannel(function(session) { 40 | console.log('New session: ' + session.id + 41 | ' from ' + session.address + 42 | ' with cookies ' + session.headers.cookie); 43 | 44 | session.on('message', function(data) { 45 | console.log(session.id + ' sent ' + JSON.stringify(data)); 46 | session.send(data); 47 | }); 48 | 49 | session.on('close', function(reason) { 50 | console.log(session.id + ' disconnected (' + reason + ')'); 51 | }); 52 | 53 | // This tells the session to disconnect and don't reconnect 54 | //session.stop(); 55 | 56 | // This kills the session. 57 | //session.close(); 58 | }) 59 | ); 60 | 61 | server.listen(4444); 62 | 63 | console.log('Echo server listening on localhost:4444'); 64 | ``` 65 | 66 | The client emulates the [websocket API](http://dev.w3.org/html5/websockets/). Here is a simple client: 67 | 68 | ```javascript 69 | var BCSocket = require('browserchannel').BCSocket; 70 | 71 | var socket = new BCSocket('http://localhost:4321/channel'); 72 | socket.onopen = function() { 73 | socket.send({hi:'there'}); 74 | }; 75 | socket.onmessage = function(message) { 76 | console.log('got message', message); 77 | }; 78 | 79 | // later... 80 | socket.close() 81 | ``` 82 | 83 | ... Or from a website: 84 | 85 | ```html 86 | 87 | 88 | 98 | ``` 99 | 100 | You can also ask the client to automatically reconnect whenever its been disconnected. - Which is 101 | super useful. 102 | 103 | ```javascript 104 | var BCSocket = require('browserchannel').BCSocket; 105 | socket = new BCSocket('http://localhost:4321/channel', reconnect:true); 106 | socket.onopen = function() { 107 | socket.send("I just connected!"); 108 | }; 109 | ``` 110 | 111 | --- 112 | 113 | # Differences from Websocket 114 | 115 | - You can send messages before the client has connected. This is recommended, 116 | as any messages sent synchronously with the connection's creation will be 117 | sent during the initial request. This removes an extra round-trip. 118 | - The send method can pass a callback which will be called when the message has 119 | been received. **NOTE**: If the client closes, it is not guaranteed that this 120 | method will ever be called. 121 | - Send uses google's JSON encoder. Its almost the same as the browser one, but 122 | `{x:undefined}` turns in to `{x:null}` not `{}`. 123 | 124 | # API 125 | 126 | ## Server API 127 | 128 | The server is created as connect / express middleware. You create the middleware by calling 129 | 130 | ```javascript 131 | var browserChannel = require('browserchannel').server; 132 | 133 | var middleware = browserChannel(options, function(session) { 134 | ... 135 | }); 136 | 137 | // Express 138 | app.use(middleware); 139 | ``` 140 | 141 | The options object is optional. The following server options are supported: 142 | 143 | - **hostPrefixes**: Array of extra subdomain prefixes on which clients can 144 | connect. Even modern browsers impose per-domain connection limits, which means 145 | that when you have a few tabs open with browserchannel requests your 146 | connections might stop woroking. Use subdomains to get around this limit. 147 | For example, if you're listening for connections on *example.com*, you can 148 | specify `hostPrefixes: ['a', 'b', 'c']` to make clients send requests to 149 | *a.example.com*, *b.example.com* and *c.example.com*. 150 | - **base**: The base URL on which to listen for connections. (Defaults to 151 | `"/channel"`). Think of the base URL as a URL passed into `app.use(url, 152 | middleware)`. 153 | - **headers**: Map of additional response headers to send with requests. 154 | - **cors**: Set `Access-Control-Allow-Origin` header. This allows you to 155 | specify a domain which is allowed to access the browserchannel server. See 156 | [mozilla documentation](https://developer.mozilla.org/en/http_access_control) 157 | for more information. You can set this to `'*'` to allow your server to be 158 | accessed from clients on any domain, but this may open up security 159 | vulnerabilities in your application as malicious sites could get users to 160 | connect to your server and send arbitrary messages. 161 | - **corsAllowCredentials**: (Default *false*) Sets the 162 | `Access-Control-Allow-Credentials` header in responses. This allows 163 | cross-domain requests to send their cookies. You cannot do this if you set 164 | `cors:'*'`. To make this work you must also add the `{crossDomainXhr:true}` 165 | option in the client. See [mozilla 166 | documentation](https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS#Requests_with_credentials) 167 | for more information. Setting this is equivalent to setting 168 | `headers:{'Access-Control-Allow-Credentials':true}`. 169 | - **keepAliveInterval**: (Default *20000 = 20 seconds*). Keepalives 170 | are sent periodically to make sure http connections aren't closed by eager 171 | clients. The standard timeout is 30 seconds, so sending keepalives every 20 172 | seconds keeps the connection healthy. Time is specified in milliseconds 173 | - **sessionTimeoutInterval**: (Default *30 seconds*). Amount of time we wait 174 | before evicting a client connection. Setting this lower will make the server 175 | notice disconnected clients more quickly. Setting it higher will make 176 | connections more resiliant to temporary network disconnection. Time specified 177 | in milliseconds. 178 | 179 | Note that by default, CORS support is disabled. This follows the lead of 180 | browsers. Be very careful when enabling CORS & CORS credentials. You should 181 | explicitly whitelist sites from which your users will connect. 182 | 183 | Setting hostPrefixes in production is important - if you don't enable host 184 | prefixes, browserchannel will stop working for a user once they have more than 185 | a couple of tabs open. Set DNS rules to make the extra prefixes all point to 186 | the same server / cluster. 187 | 188 | ### Client sessions 189 | 190 | Whenever a client connects, your middleware's method is called with the new 191 | session. The session is a nodejs event emitter with the following properties: 192 | 193 | - **id**: An ID that is unique to the session. 194 | - **address**: A string containing the IP address of the connecting client. 195 | - **query**: An object containing the parsed HTTP query of the initial 196 | connection. Any custom query parameters will be exposed on this object. 197 | - **state**: The current state of the connection. One of `'init'`, `'ok'` and 198 | `'closed'`. When the state is changed, the client will emit a *state changed* 199 | event with the new state and old state as event parameters. 200 | - **appVersion**: The client's reported application version, or null. You can 201 | use this to reject clients which are connecting from old versions of your 202 | client. 203 | 204 | #### Sending messages 205 | 206 | You can **send messages** to the client using `client.send(data, callback)`. The 207 | data parameter will be automatically JSON.stringified. If specified, the 208 | callback will be called once the message has been acknowledged by the client. 209 | 210 | > Note: If you wrap a browserchannel connection in a nodejs stream, don't use 211 | > the callback. Node streams will only allow one message to be in flight at a 212 | > time. As a result, you'll get much lower message throughput than you 213 | > otherwise should. 214 | 215 | 216 | #### Receiving messages 217 | 218 | Receive messages through the `message` event. 219 | 220 | ```javascript 221 | session.on('message', function(data) { 222 | // ... 223 | }); 224 | ``` 225 | 226 | The message will be a javascript object if you sent a javascript object using 227 | the client API. 228 | 229 | #### Stopping and closing connections 230 | 231 | Browserchannel has two different methods for closing client connections, *session.stop* 232 | and *session.close*. Both methods disconnect the client. The difference is that 233 | stop also tells the client not to reconnect. You should use close when a 234 | recoverable server error occurs, and stop when the client is in an 235 | unrecoverable invalid state. 236 | 237 | For example, if an exception occurs handling a message from a client, you may 238 | want to call close() to force the client to reconnect. On the other hand, if a 239 | browser is trying to connect using an old version of your app, you should call 240 | stop(). In the browser, you can handle the stop message with a notice to 241 | refresh the browser tab. 242 | 243 | #### Events 244 | 245 | The client is an event emitter. It fires the following events: 246 | 247 | - **close (reason)**: The client connection was closed. This will happen for a variety 248 | of reasons (timeouts, explicit disconnection from the client, disconnection 249 | from the server, etc). Once a client has closed, it is gone forever. If the 250 | client reconnects, it will do so by establishing a new session. 251 | - **message (data)**: The server received a message from the client. The data 252 | object will be a javascript object. 253 | - **state changed (newstate, oldstate)**: The client's state changed. Clients 254 | start in the 'init' state. They move to the 'ok' state when the session is 255 | established then go to the 'closed' state. If a client reconnects, they will 256 | create an entirely new session. init -> ok -> closed are the only three valid 257 | state transitions. 258 | 259 | 260 | ## Client API 261 | 262 | For the most part, the client API is identical to websockets. 263 | 264 | ```javascript 265 | var socket = new BCSocket(hostname, opts); 266 | ``` 267 | 268 | opts is optional, and if it exists it should be an object which can contain the 269 | following properties: 270 | 271 | - **appVersion**: Your application's protocol version. This is passed to the server-side 272 | browserchannel code, in through your session handler as `session.appVersion` 273 | - **prev**: The previous BCSocket object, if one exists. When the socket is established, 274 | the previous bcsocket session will be disconnected as we reconnect. 275 | - **reconnect**: Tell the socket to automatically reconnect when its been disconnected. 276 | - **failFast**: Make the socket report errors immediately, rather than trying a 277 | few times first. 278 | - **crossDomainXhr**: Set to true to enable the cross-origin credential 279 | flags in XHR requests. The server must send the 280 | Access-Control-Allow-Credentials header and can't use wildcard access 281 | control hostnames. See: 282 | http://www.html5rocks.com/en/tutorials/cors/#toc-withcredentials 283 | - **affinity**: Set to null to disable session affinity token passing. 284 | - **affinityParam**: Session affinity tokens are sent in the query string as 285 | the GET parameter `a` by default. Your application may override the 286 | variable name if there is a query string conflict. 287 | 288 | There are a couple of differences from the websocket API: 289 | 290 | - You can (and are encouraged to) call `send()` using JSON objects instead of 291 | mere strings. JSON serialization is handled by the library, and works in all 292 | browsers. 293 | - You can send messages immediately before the session is established. This 294 | removes a roundtrip before client messages can arrive on the server. 295 | - Browserchannel sessions have reconnect support. You should register 296 | `socket.onconnecting = function() {...}` to send any messages which need to be 297 | sent as the session is established. This will be called both when the socket is 298 | first established *and* when the session reconnects. 299 | 300 | 301 | --- 302 | 303 | # Caveats 304 | 305 | - It doesn't do RPC. 306 | - Currently there's no websocket support. So, its higher bandwidth on modern 307 | browsers. On iOS you'll sometimes see a perpetual loading spinner in the top 308 | black bar. 309 | 310 | --- 311 | 312 | # How to rebuild the client 313 | 314 | The client uses google's [closure library](https://developers.google.com/closure/library/) 315 | & [compiler](https://developers.google.com/closure/compiler/). There's a couple small bugs that google 316 | still hasn't fixed in their library (and probably never will), so I have a patch file kicking around. 317 | 318 | Rebuilding the client library is annoying, so I keep an up to date compiled copy in `dist/`. 319 | 320 | 1. Download the closure library as a sibling of this repository 321 | 322 | ``` 323 | cd .. 324 | git clone https://code.google.com/p/closure-library/ 325 | git checkout -q df47692b1bacd494548a3b00b150d9f6a428d58a 326 | cd closure-library 327 | ``` 328 | 329 | 2. Download the closure compiler 330 | 331 | ``` 332 | curl http://dl.google.com/closure-compiler/compiler-latest.tar.gz > compiler-latest.tar.gz 333 | tar -xvf compiler-latest.tar.gz 334 | mv compiler-latest/compiler.jar . 335 | ``` 336 | 337 | 3. Patch the library 338 | 339 | ``` 340 | cd closure/ 341 | patch -p0 < ../../node-browserchannel/closure-*.patch 342 | ``` 343 | 344 | 4. Build 345 | 346 | ``` 347 | cd ../../node-browserchannel 348 | make 349 | ``` 350 | 351 | ## Caveats 352 | 353 | ### Java ~1.7 is a hard requirement. 354 | Building this project with Java ~1.6 will fail, and may even fail silently. 355 | 356 | ### Known issue with latest closure-library. 357 | Until [the bug][34] introduced in `closure-library#83c6a0b9` 358 | is resolved upstream, use `closure-library#df47692` 359 | 360 | [34]: https://github.com/josephg/node-browserchannel/issues/34 361 | 362 | 363 | --- 364 | 365 | ### License 366 | 367 | Licensed under the standard MIT license: 368 | 369 | Copyright 2011 Joseph Gentle. 370 | 371 | Permission is hereby granted, free of charge, to any person obtaining a copy 372 | of this software and associated documentation files (the "Software"), to deal 373 | in the Software without restriction, including without limitation the rights 374 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 375 | copies of the Software, and to permit persons to whom the Software is 376 | furnished to do so, subject to the following conditions: 377 | 378 | The above copyright notice and this permission notice shall be included in 379 | all copies or substantial portions of the Software. 380 | 381 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 382 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 383 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 384 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 385 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 386 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 387 | THE SOFTWARE. 388 | -------------------------------------------------------------------------------- /XMLHttpRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object. 3 | * 4 | * This can be used with JS designed for browsers to improve reuse of code and 5 | * allow the use of existing libraries. 6 | * 7 | * Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs. 8 | * 9 | * @todo SSL Support 10 | * @author Dan DeFelippi 11 | * @contributor David Ellis 12 | * @license MIT 13 | * 14 | * This has been modified by Joseph Gentle to fix some bugs. 15 | * It will live here until the changes have been merged into node-XMLHttpRequest 16 | * upstream. 17 | */ 18 | 19 | var Url = require("url"), 20 | spawn = require("child_process").spawn, 21 | fs = require('fs'); 22 | 23 | exports.XMLHttpRequest = function() { 24 | /** 25 | * Private variables 26 | */ 27 | var self = this; 28 | var http = require('http'); 29 | var https = require('https'); 30 | 31 | // Holds http.js objects 32 | var client; 33 | var request; 34 | var response; 35 | 36 | // Request settings 37 | var settings = {}; 38 | 39 | // Set some default headers 40 | var defaultHeaders = { 41 | "User-Agent": "node.js", 42 | "Accept": "*/*", 43 | }; 44 | 45 | // Send flag 46 | var sendFlag = false; 47 | 48 | var headers = defaultHeaders; 49 | 50 | /** 51 | * Constants 52 | */ 53 | this.UNSENT = 0; 54 | this.OPENED = 1; 55 | this.HEADERS_RECEIVED = 2; 56 | this.LOADING = 3; 57 | this.DONE = 4; 58 | 59 | /** 60 | * Public vars 61 | */ 62 | // Current state 63 | this.readyState = this.UNSENT; 64 | 65 | // default ready state change handler in case one is not set or is set late 66 | this.onreadystatechange = null; 67 | 68 | // Result & response 69 | this.responseText = ""; 70 | this.responseXML = ""; 71 | this.status = null; 72 | this.statusText = null; 73 | 74 | /** 75 | * Open the connection. Currently supports local server requests. 76 | * 77 | * @param string method Connection method (eg GET, POST) 78 | * @param string url URL for the connection. 79 | * @param boolean async Asynchronous connection. Default is true. 80 | * @param string user Username for basic authentication (optional) 81 | * @param string password Password for basic authentication (optional) 82 | */ 83 | this.open = function(method, url, async, user, password) { 84 | settings = { 85 | "method": method, 86 | "url": url.toString(), 87 | "async": async || true, 88 | "user": user || null, 89 | "password": password || null 90 | }; 91 | 92 | this.abort(); 93 | 94 | setState(this.OPENED); 95 | }; 96 | 97 | /** 98 | * Sets a header for the request. 99 | * 100 | * @param string header Header name 101 | * @param string value Header value 102 | */ 103 | this.setRequestHeader = function(header, value) { 104 | headers[header] = value; 105 | }; 106 | 107 | /** 108 | * Gets a header from the server response. 109 | * 110 | * @param string header Name of header to get. 111 | * @return string Text of the header or null if it doesn't exist. 112 | */ 113 | this.getResponseHeader = function(header) { 114 | if (this.readyState > this.OPENED && response.headers[header]) { 115 | return response.headers[header]; 116 | } 117 | 118 | return null; 119 | }; 120 | 121 | /** 122 | * Gets all the response headers. 123 | * 124 | * @return string 125 | */ 126 | this.getAllResponseHeaders = function() { 127 | if (this.readyState < this.HEADERS_RECEIVED) { 128 | throw "INVALID_STATE_ERR: Headers have not been received."; 129 | } 130 | var result = ""; 131 | 132 | for (var i in response.headers) { 133 | result += i + ": " + response.headers[i] + "\r\n"; 134 | } 135 | return result.substr(0, result.length - 2); 136 | }; 137 | 138 | /** 139 | * Sends the request to the server. 140 | * 141 | * @param string data Optional data to send as request body. 142 | */ 143 | this.send = function(data) { 144 | if (this.readyState != this.OPENED) { 145 | throw "INVALID_STATE_ERR: connection must be opened before send() is called"; 146 | } 147 | 148 | if (sendFlag) { 149 | throw "INVALID_STATE_ERR: send has already been called"; 150 | } 151 | 152 | var ssl = false; 153 | var url = Url.parse(settings.url); 154 | 155 | // Determine the server 156 | switch (url.protocol) { 157 | case 'https:': 158 | ssl = true; 159 | // SSL & non-SSL both need host, no break here. 160 | case 'http:': 161 | var host = url.hostname; 162 | break; 163 | 164 | case undefined: 165 | case '': 166 | var host = "localhost"; 167 | break; 168 | 169 | default: 170 | throw "Protocol not supported."; 171 | } 172 | 173 | // Default to port 80. If accessing localhost on another port be sure 174 | // to use http://localhost:port/path 175 | var port = url.port || (ssl ? 443 : 80); 176 | // Add query string if one is used 177 | var uri = url.pathname + (url.search ? url.search : ''); 178 | 179 | // Set the Host header or the server may reject the request 180 | this.setRequestHeader("Host", host); 181 | 182 | // Set Basic Auth if necessary 183 | if (settings.user) { 184 | if (typeof settings.password == "undefined") { 185 | settings.password = ""; 186 | } 187 | var authBuf = new Buffer(settings.user + ":" + settings.password); 188 | headers["Authorization"] = "Basic " + authBuf.toString("base64"); 189 | } 190 | 191 | // Set content length header 192 | if (settings.method == "GET" || settings.method == "HEAD") { 193 | data = null; 194 | } else if (data) { 195 | this.setRequestHeader("Content-Length", Buffer.byteLength(data)); 196 | 197 | if (!headers["Content-Type"]) { 198 | this.setRequestHeader("Content-Type", "text/plain;charset=UTF-8"); 199 | } 200 | } 201 | 202 | var options = { 203 | host: host, 204 | port: port, 205 | path: uri, 206 | method: settings.method, 207 | headers: headers 208 | }; 209 | 210 | if(!settings.hasOwnProperty("async") || settings.async) { //Normal async path 211 | // Use the proper protocol 212 | var doRequest = ssl ? https.request : http.request; 213 | 214 | sendFlag = true; 215 | 216 | // As per spec, this is called here for historical reasons. 217 | if (typeof self.onreadystatechange === "function") { 218 | self.onreadystatechange(); 219 | } 220 | 221 | request = doRequest(options, function(resp) { 222 | response = resp; 223 | response.setEncoding("utf8"); 224 | 225 | setState(self.HEADERS_RECEIVED); 226 | self.status = response.statusCode; 227 | 228 | response.on('data', function(chunk) { 229 | // Make sure there's some data 230 | if (chunk) { 231 | self.responseText += chunk; 232 | } 233 | // Don't emit state changes if the connection has been aborted. 234 | if (sendFlag) { 235 | setState(self.LOADING); 236 | } 237 | }); 238 | 239 | response.on('end', function() { 240 | if (sendFlag) { 241 | // Discard the 'end' event if the connection has been aborted 242 | setState(self.DONE); 243 | sendFlag = false; 244 | } 245 | }); 246 | 247 | response.on('error', function(error) { 248 | self.handleError(error); 249 | }); 250 | }).on('error', function(error) { 251 | self.handleError(error); 252 | }); 253 | 254 | // Node 0.4 and later won't accept empty data. Make sure it's needed. 255 | if (data) { 256 | request.write(data); 257 | } 258 | 259 | request.end(); 260 | } else { // Synchronous 261 | // Create a temporary file for communication with the other Node process 262 | var syncFile = ".node-xmlhttprequest-sync-" + process.pid; 263 | fs.writeFileSync(syncFile, "", "utf8"); 264 | // The async request the other Node process executes 265 | var execString = "var http = require('http'), https = require('https'), fs = require('fs');" 266 | + "var doRequest = http" + (ssl?"s":"") + ".request;" 267 | + "var options = " + JSON.stringify(options) + ";" 268 | + "var responseText = '';" 269 | + "var req = doRequest(options, function(response) {" 270 | + "response.setEncoding('utf8');" 271 | + "response.on('data', function(chunk) {" 272 | + "responseText += chunk;" 273 | + "});" 274 | + "response.on('end', function() {" 275 | + "fs.writeFileSync('" + syncFile + "', 'NODE-XMLHTTPREQUEST-STATUS:' + response.statusCode + ',' + responseText, 'utf8');" 276 | + "});" 277 | + "response.on('error', function(error) {" 278 | + "fs.writeFileSync('" + syncFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" 279 | + "});" 280 | + "}).on('error', function(error) {" 281 | + "fs.writeFileSync('" + syncFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" 282 | + "});" 283 | + (data ? "req.write('" + data.replace(/'/g, "\\'") + "');":"") 284 | + "req.end();"; 285 | // Start the other Node Process, executing this string 286 | syncProc = spawn(process.argv[0], ["-e", execString]); 287 | while((self.responseText = fs.readFileSync(syncFile, 'utf8')) == "") { 288 | // Wait while the file is empty 289 | } 290 | // Kill the child process once the file has data 291 | syncProc.stdin.end(); 292 | // Remove the temporary file 293 | fs.unlinkSync(syncFile); 294 | if(self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) { 295 | // If the file returned an error, handle it 296 | var errorObj = self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""); 297 | self.handleError(errorObj); 298 | } else { 299 | // If the file returned okay, parse its data and move to the DONE state 300 | self.status = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, "$1"); 301 | self.responseText = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, "$1"); 302 | setState(self.DONE); 303 | } 304 | } 305 | }; 306 | 307 | this.handleError = function(error) { 308 | this.status = 503; 309 | this.statusText = error; 310 | this.responseText = error.stack; 311 | setState(this.DONE); 312 | }; 313 | 314 | /** 315 | * Aborts a request. 316 | */ 317 | this.abort = function() { 318 | headers = defaultHeaders; 319 | this.responseText = ""; 320 | this.responseXML = ""; 321 | 322 | if (request) { 323 | request.abort(); 324 | request = null; 325 | } 326 | 327 | if (this.readyState !== this.UNSENT 328 | && (this.readyState !== this.OPENED || sendFlag) 329 | && this.readyState !== this.DONE) { 330 | sendFlag = false; 331 | setState(this.DONE); 332 | } 333 | this.readyState = this.UNSENT; 334 | }; 335 | 336 | /** 337 | * Changes readyState and calls onreadystatechange. 338 | * 339 | * @param int state New state 340 | */ 341 | var setState = function(state) { 342 | self.readyState = state; 343 | if (typeof self.onreadystatechange === "function") { 344 | self.onreadystatechange(); 345 | } 346 | }; 347 | }; 348 | -------------------------------------------------------------------------------- /closure-terminate-fixes-r2519.patch: -------------------------------------------------------------------------------- 1 | Index: goog/net/tmpnetwork.js 2 | =================================================================== 3 | --- goog/net/tmpnetwork.js (revision 2519) 4 | +++ goog/net/tmpnetwork.js (working copy) 5 | @@ -103,44 +103,26 @@ 6 | var channelDebug = new goog.net.ChannelDebug(); 7 | channelDebug.debug('TestLoadImage: loading ' + url); 8 | var img = new Image(); 9 | - img.onload = function() { 10 | - try { 11 | - channelDebug.debug('TestLoadImage: loaded'); 12 | - goog.net.tmpnetwork.clearImageCallbacks_(img); 13 | - callback(true); 14 | - } catch (e) { 15 | - channelDebug.dumpException(e); 16 | - } 17 | + var timer = null; 18 | + var createHandler = function(result, message) { 19 | + return function() { 20 | + try { 21 | + channelDebug.debug('TestLoadImage: ' + message); 22 | + goog.net.tmpnetwork.clearImageCallbacks_(img); 23 | + goog.global.clearTimeout(timer); 24 | + callback(result); 25 | + } catch (e) { 26 | + channelDebug.dumpException(e); 27 | + } 28 | + }; 29 | }; 30 | - img.onerror = function() { 31 | - try { 32 | - channelDebug.debug('TestLoadImage: error'); 33 | - goog.net.tmpnetwork.clearImageCallbacks_(img); 34 | - callback(false); 35 | - } catch (e) { 36 | - channelDebug.dumpException(e); 37 | - } 38 | - }; 39 | - img.onabort = function() { 40 | - try { 41 | - channelDebug.debug('TestLoadImage: abort'); 42 | - goog.net.tmpnetwork.clearImageCallbacks_(img); 43 | - callback(false); 44 | - } catch (e) { 45 | - channelDebug.dumpException(e); 46 | - } 47 | - }; 48 | - img.ontimeout = function() { 49 | - try { 50 | - channelDebug.debug('TestLoadImage: timeout'); 51 | - goog.net.tmpnetwork.clearImageCallbacks_(img); 52 | - callback(false); 53 | - } catch (e) { 54 | - channelDebug.dumpException(e); 55 | - } 56 | - }; 57 | 58 | - goog.global.setTimeout(function() { 59 | + img.onload = createHandler(true, 'loaded'); 60 | + img.onerror = createHandler(false, 'error'); 61 | + img.onabort = createHandler(false, 'abort'); 62 | + img.ontimeout = createHandler(false, 'timeout'); 63 | + 64 | + timer = goog.global.setTimeout(function() { 65 | if (img.ontimeout) { 66 | img.ontimeout(); 67 | } 68 | Index: goog/net/channelrequest.js 69 | =================================================================== 70 | --- goog/net/channelrequest.js (revision 2519) 71 | +++ goog/net/channelrequest.js (working copy) 72 | @@ -1037,10 +1037,20 @@ 73 | goog.net.ChannelRequest.prototype.imgTagGet_ = function() { 74 | var eltImg = new Image(); 75 | eltImg.src = this.baseUri_; 76 | + eltImg.onload = eltImg.onerror = goog.bind(this.imgTagComplete_, this); 77 | this.requestStartTime_ = goog.now(); 78 | this.ensureWatchDogTimer_(); 79 | }; 80 | 81 | +/** 82 | + * Callback when the image request is complete 83 | + * 84 | + * @private 85 | + */ 86 | +goog.net.ChannelRequest.prototype.imgTagComplete_ = function() { 87 | + this.cancelWatchDogTimer_(); 88 | + this.channel_.onRequestComplete(this); 89 | +} 90 | 91 | /** 92 | * Cancels the request no matter what the underlying transport is. 93 | -------------------------------------------------------------------------------- /dist/bcsocket.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var f,aa=aa||{},l=this;function ba(a){a=a.split(".");for(var b=l,c;c=a.shift();)if(null!=b[c])b=b[c];else return null;return b}function ca(){} 3 | function da(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null"; 4 | else if("function"==b&&"undefined"==typeof a.call)return"object";return b}function m(a){return"array"==da(a)}function ea(a){var b=da(a);return"array"==b||"object"==b&&"number"==typeof a.length}function n(a){return"string"==typeof a}function fa(a){return"function"==da(a)}var ga="closure_uid_"+(1E9*Math.random()>>>0),ha=0;function ia(a,b,c){return a.call.apply(a.bind,arguments)} 5 | function ja(a,b,c){if(!a)throw Error();if(2")&&(a=a.replace(pa,">"));-1!=a.indexOf('"')&&(a=a.replace(qa,"""));-1!=a.indexOf("'")&&(a=a.replace(ra,"'"));return a}var na=/&/g,oa=//g,qa=/"/g,ra=/'/g,ma=/[&<>"']/; 7 | function sa(){return Math.floor(2147483648*Math.random()).toString(36)+Math.abs(Math.floor(2147483648*Math.random())^q()).toString(36)}function ta(a,b){return ab?1:0};var x,ua,va,wa;function xa(){return l.navigator?l.navigator.userAgent:null}wa=va=ua=x=!1;var ya;if(ya=xa()){var za=l.navigator;x=0==ya.lastIndexOf("Opera",0);ua=!x&&(-1!=ya.indexOf("MSIE")||-1!=ya.indexOf("Trident"));va=!x&&-1!=ya.indexOf("WebKit");wa=!x&&!va&&!ua&&"Gecko"==za.product}var Aa=x,y=ua,Ba=wa,z=va;function Ca(){var a=l.document;return a?a.documentMode:void 0}var Da; 8 | a:{var Ea="",Fa;if(Aa&&l.opera)var Ga=l.opera.version,Ea="function"==typeof Ga?Ga():Ga;else if(Ba?Fa=/rv\:([^\);]+)(\)|;)/:y?Fa=/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/:z&&(Fa=/WebKit\/(\S+)/),Fa)var Ha=Fa.exec(xa()),Ea=Ha?Ha[1]:"";if(y){var Ia=Ca();if(Ia>parseFloat(Ea)){Da=String(Ia);break a}}Da=Ea}var Ja={}; 9 | function A(a){var b;if(!(b=Ja[a])){b=0;for(var c=String(Da).replace(/^[\s\xa0]+|[\s\xa0]+$/g,"").split("."),d=String(a).replace(/^[\s\xa0]+|[\s\xa0]+$/g,"").split("."),e=Math.max(c.length,d.length),g=0;0==b&&gc?Math.max(0,a.length+c):c;if(n(a))return n(b)&&1==b.length?a.indexOf(b,c):-1;for(;cb?null:n(a)?a.charAt(b):a[b]}function ab(a){return B.concat.apply(B,arguments)}function bb(a){var b=a.length;if(02*this.o&&db(this),!0):!1};function db(a){if(a.o!=a.j.length){for(var b=0,c=0;bb)throw Error("Bad port number "+b);a.Ca=b}else a.Ca=null}function ib(a,b,c){F(a);a.I=c?b?decodeURIComponent(b):"":b}function jb(a,b,c){F(a);b instanceof kb?(a.R=b,a.R.ub(a.D)):(c||(b=lb(b,qb)),a.R=new kb(b,0,a.D))}function G(a,b,c){F(a);a.R.set(b,c)} 16 | function rb(a,b,c){F(a);m(c)||(c=[String(c)]);sb(a.R,b,c)}function H(a){F(a);G(a,"zx",sa());return a}function F(a){if(a.oc)throw Error("Tried to modify a read-only Uri");}f.ub=function(a){this.D=a;this.R&&this.R.ub(a);return this};function tb(a){return a instanceof E?a.n():new E(a,void 0)}function ub(a,b,c,d){var e=new E(null,void 0);a&&fb(e,a);b&&gb(e,b);c&&hb(e,c);d&&ib(e,d);return e}function lb(a,b){return n(a)?encodeURI(a).replace(b,vb):null} 17 | function vb(a){a=a.charCodeAt(0);return"%"+(a>>4&15).toString(16)+(a&15).toString(16)}var mb=/[#\/\?@]/g,ob=/[\#\?:]/g,nb=/[\#\?]/g,qb=/[\#\?@]/g,pb=/#/g;function kb(a,b,c){this.C=a||null;this.D=!!c}function I(a){if(!a.h&&(a.h=new cb,a.o=0,a.C))for(var b=a.C.split("&"),c=0;cb?e+="000":256>b?e+="00":4096>b&&(e+="0");return Cb[a]=e+b.toString(16)}),'"')};function Eb(a){return Fb(a||arguments.callee.caller,[])} 25 | function Fb(a,b){var c=[];if(0<=Xa(b,a))c.push("[...circular reference...]");else if(a&&50>b.length){c.push(Gb(a)+"(");for(var d=a.arguments,e=0;e=Qb(this).value)for(fa(b)&&(b=b()),a=this.mc(a,b,c),b="log:"+a.qc,l.console&&(l.console.timeStamp?l.console.timeStamp(b):l.console.markTimeline&&l.console.markTimeline(b)),l.msWriteProfilerMark&&l.msWriteProfilerMark(b),b=this;b;){c=b;var d=a;if(c.Jb)for(var e=0,g=void 0;g=c.Jb[e];e++)g(d);b=b.getParent()}}; 28 | f.mc=function(a,b,c){var d=new Ib(a,String(b),this.rc);if(c){d.Fb=c;var e;var g=arguments.callee.caller;try{var h;var k=ba("window.location.href");if(n(c))h={message:c,name:"Unknown error",lineNumber:"Not available",fileName:k,stack:"Not available"};else{var u,K,v=!1;try{u=c.lineNumber||c.Ic||"Not available"}catch(r){u="Not available",v=!0}try{K=c.fileName||c.filename||c.sourceURL||l.$googDebugFname||k}catch(Ka){K="Not available",v=!0}h=!v&&c.lineNumber&&c.fileName&&c.stack&&c.message&&c.name?c:{message:c.message|| 29 | "Not available",name:c.name||"UnknownError",lineNumber:u,fileName:K,stack:c.stack||"Not available"}}e="Message: "+la(h.message)+'\nUrl: '+h.fileName+"\nLine: "+h.lineNumber+"\n\nBrowser stack:\n"+la(h.stack+"-> ")+"[end]\n\nJS stack traversal:\n"+la(Eb(g)+"-> ")}catch(w){e="Exception trying to expose exception! You win, we lose. "+w}d.Eb=e}return d};f.J=function(a,b){this.log(Lb,a,b)};f.Z=function(a,b){this.log(Mb,a,b)}; 30 | f.info=function(a,b){this.log(Nb,a,b)};var Rb={},Sb=null;function Tb(a){Sb||(Sb=new L(""),Rb[""]=Sb,Sb.$b(Ob));var b;if(!(b=Rb[a])){b=new L(a);var c=a.lastIndexOf("."),d=a.substr(c+1),c=Tb(a.substr(0,c));c.jb||(c.jb={});c.jb[d]=b;b.Sa=c;Rb[a]=b}return b};function M(a,b){a&&a.log(Pb,b,void 0)};function N(){this.r=Tb("goog.net.BrowserChannel")}function Ub(a,b,c,d){a.info("XMLHTTP TEXT ("+b+"): "+Vb(a,c)+(d?" "+d:""))}N.prototype.debug=function(a){this.info(a)};function Wb(a,b,c){a.J((c||"Exception")+b)}N.prototype.info=function(a){var b=this.r;b&&b.info(a,void 0)};N.prototype.Z=function(a){var b=this.r;b&&b.Z(a,void 0)};N.prototype.J=function(a){var b=this.r;b&&b.J(a,void 0)}; 31 | function Vb(a,b){if(!b||b==Xb)return b;try{var c=xb(b);if(c)for(var d=0;de.length)){var g=e[1];if(m(g)&&!(1>g.length)){var h=g[0];if("noop"!=h&&"stop"!=h)for(var k=1;k=a.keyCode)a.keyCode=-1}catch(b){}};kc.prototype.u=function(){};var lc="closure_lm_"+(1E6*Math.random()|0),mc={},nc=0;function oc(a,b,c,d,e){if(m(b)){for(var g=0;gc.keyCode||void 0!=c.returnValue)){a:{var g=!1;if(0==c.keyCode)try{c.keyCode=-1;break a}catch(h){g=!0}if(g||void 0==c.returnValue)c.returnValue=!0}c=[];for(g=d.currentTarget;g;g=g.parentNode)c.push(g);for(var g=a.type,k=c.length-1;!d.ga&&0<=k;k--)d.currentTarget=c[k],e&=vc(c[k],g,!0,d);for(k=0;!d.ga&&k>>0);function pc(a){return fa(a)?a:a[xc]||(a[xc]=function(b){return a.handleEvent(b)})};function R(){O.call(this);this.W=new P(this);this.fc=this}s(R,O);R.prototype[ac]=!0;f=R.prototype;f.tb=null;f.addEventListener=function(a,b,c,d){oc(this,a,b,c,d)};f.removeEventListener=function(a,b,c,d){tc(this,a,b,c,d)}; 41 | f.dispatchEvent=function(a){var b,c=this.tb;if(c)for(b=[];c;c=c.tb)b.push(c);var c=this.fc,d=a.type||a;if(n(a))a=new Q(a,c);else if(a instanceof Q)a.target=a.target||c;else{var e=a;a=new Q(d,c);Wa(a,e)}var e=!0,g;if(b)for(var h=b.length-1;!a.ga&&0<=h;h--)g=a.currentTarget=b[h],e=yc(g,d,!0,a)&&e;a.ga||(g=a.currentTarget=c,e=yc(g,d,!0,a)&&e,a.ga||(e=yc(g,d,!1,a)&&e));if(b)for(h=0;!a.ga&&hb)break a}else if(3>b||3==b&&!Aa&&!Tc(this.k))break a;this.$||4!=b||7==c||(8==c||0>=d?this.b.H(kd):this.b.H(ld));md(this);var e=Sc(this.k);this.g=e;var g=Tc(this.k);g||this.a.debug("No response text for uri "+this.w+" status "+e);this.F=200==e;this.a.info("XMLHTTP RESP ("+this.B+") [ attempt "+this.Ea+"]: "+this.ta+"\n"+this.w+"\n"+b+" "+e);this.F?(4==b&&V(this),this.Cb?(nd(this,b,g),Aa&&this.F&& 58 | 3==b&&(this.ob.Ra(this.Ta,Ac,this.Ac),this.Ta.start())):(Ub(this.a,this.B,g,null),od(this,g)),this.F&&!this.$&&(4==b?this.b.la(this):(this.F=!1,id(this)))):(400==e&&0b.length)return dd;var e=b.substr(d,c);a.Ha=d+c;return e} 61 | function rd(a,b){a.Da=q();id(a);var c=b?window.location.hostname:"";a.w=a.U.n();G(a.w,"DOMAIN",c);G(a.w,"t",a.Ea);try{a.K=new ActiveXObject("htmlfile")}catch(d){a.a.J("ActiveX blocked");V(a);a.q=7;W();pd(a);return}var e="";b&&(e+='\n"); 68 | } 69 | }, 70 | write: function(data) { 71 | res.write("\n"); 72 | if (!junkSent) { 73 | res.write(ieJunk); 74 | return junkSent = true; 75 | } 76 | }, 77 | end: function() { 78 | return res.end("\n"); 79 | }, 80 | writeError: function(statusCode, message) { 81 | methods.writeHead(); 82 | return res.end("\n"); 83 | } 84 | }; 85 | methods.writeRaw = methods.write; 86 | return methods; 87 | } else { 88 | return { 89 | writeHead: function() { 90 | return res.writeHead(200, 'OK', options.headers); 91 | }, 92 | write: function(data) { 93 | return res.write("" + data.length + "\n" + data); 94 | }, 95 | writeRaw: function(data) { 96 | return res.write(data); 97 | }, 98 | end: function() { 99 | return res.end(); 100 | }, 101 | writeError: function(statusCode, message) { 102 | res.writeHead(statusCode, options.headers); 103 | return res.end(message); 104 | } 105 | }; 106 | } 107 | }; 108 | 109 | sendError = function(res, statusCode, message, options) { 110 | res.writeHead(statusCode, message, options.headers); 111 | res.end("

" + message + "

"); 112 | }; 113 | 114 | bufferPostData = function(req, callback) { 115 | var data; 116 | data = []; 117 | req.on('data', function(chunk) { 118 | return data.push(chunk.toString('utf8')); 119 | }); 120 | return req.on('end', function() { 121 | data = data.join(''); 122 | return callback(data); 123 | }); 124 | }; 125 | 126 | transformData = function(req, data) { 127 | var count, id, key, map, mapKey, maps, match, ofs, regex, val, _ref; 128 | if (req.headers['content-type'] === 'application/json') { 129 | _ref = data, ofs = _ref.ofs, data = _ref.data; 130 | return { 131 | ofs: ofs, 132 | json: data 133 | }; 134 | } else { 135 | count = parseInt(data.count); 136 | if (count === 0) { 137 | return null; 138 | } 139 | ofs = parseInt(data.ofs); 140 | if (isNaN(count || isNaN(ofs))) { 141 | throw new Error('invalid map data'); 142 | } 143 | if (!(count === 0 || (count > 0 && (data.ofs != null)))) { 144 | throw new Error('Invalid maps'); 145 | } 146 | maps = new Array(count); 147 | regex = /^req(\d+)_(.+)$/; 148 | for (key in data) { 149 | val = data[key]; 150 | match = regex.exec(key); 151 | if (match) { 152 | id = match[1]; 153 | mapKey = match[2]; 154 | map = (maps[id] || (maps[id] = {})); 155 | if (id === 'type' && mapKey === '_badmap') { 156 | continue; 157 | } 158 | map[mapKey] = val; 159 | } 160 | } 161 | return { 162 | ofs: ofs, 163 | maps: maps 164 | }; 165 | } 166 | }; 167 | 168 | decodeData = function(req, data) { 169 | if (req.headers['content-type'] === 'application/json') { 170 | return JSON.parse(data); 171 | } else { 172 | return querystring.parse(data, '&', '=', { 173 | maxKeys: 0 174 | }); 175 | } 176 | }; 177 | 178 | order = function(start, playOld) { 179 | var base, queue; 180 | base = start; 181 | queue = new Array(10); 182 | return function(seq, callback) { 183 | callback || (callback = function() {}); 184 | if (seq < base) { 185 | if (playOld) { 186 | callback(); 187 | } 188 | } else { 189 | queue[seq - base] = callback; 190 | while (queue[0]) { 191 | callback = queue.shift(); 192 | base++; 193 | callback(); 194 | } 195 | } 196 | }; 197 | }; 198 | 199 | getHostPrefix = function(options) { 200 | if (options.hostPrefixes) { 201 | return randomArrayElement(options.hostPrefixes); 202 | } else { 203 | return null; 204 | } 205 | }; 206 | 207 | clientFile = "" + __dirname + "/../dist/bcsocket.js"; 208 | 209 | clientStats = fs.statSync(clientFile); 210 | 211 | try { 212 | clientCode = fs.readFileSync(clientFile, 'utf8'); 213 | } catch (_error) { 214 | e = _error; 215 | console.error('Could not load the client javascript. Run `cake client` to generate it.'); 216 | throw e; 217 | } 218 | 219 | if (process.env.NODE_ENV !== 'production') { 220 | if (process.platform === "win32") { 221 | fs.watch(clientFile, { 222 | persistent: false 223 | }, function(event, filename) { 224 | if (event === "change") { 225 | console.log("Reloading client JS"); 226 | clientCode = fs.readFileSync(clientFile, 'utf8'); 227 | return clientStats = curr; 228 | } 229 | }); 230 | } else { 231 | fs.watchFile(clientFile, { 232 | persistent: false 233 | }, function(curr, prev) { 234 | if (curr.mtime.getTime() !== prev.mtime.getTime()) { 235 | console.log("Reloading client JS"); 236 | clientCode = fs.readFileSync(clientFile, 'utf8'); 237 | return clientStats = curr; 238 | } 239 | }); 240 | } 241 | } 242 | 243 | BCSession = function(address, query, headers, options) { 244 | EventEmitter.call(this); 245 | this.id = hat(); 246 | this.address = address; 247 | this.headers = headers; 248 | this.query = query; 249 | this.options = options; 250 | this.state = 'init'; 251 | this.appVersion = query.CVER || null; 252 | this._backChannel = null; 253 | this._outgoingArrays = []; 254 | this._lastArrayId = -1; 255 | this._lastSentArrayId = -1; 256 | this._heartbeat = null; 257 | this._sessionTimeout = null; 258 | this._refreshSessionTimeout(); 259 | this._queueArray(['c', this.id, getHostPrefix(options), 8]); 260 | this._mapBuffer = order(0, false); 261 | this._ridBuffer = order(query.RID, true); 262 | }; 263 | 264 | (function() { 265 | var method, name, _ref; 266 | _ref = EventEmitter.prototype; 267 | for (name in _ref) { 268 | method = _ref[name]; 269 | BCSession.prototype[name] = method; 270 | } 271 | })(); 272 | 273 | BCSession.prototype._changeState = function(newState) { 274 | var oldState; 275 | oldState = this.state; 276 | this.state = newState; 277 | return this.emit('state changed', this.state, oldState); 278 | }; 279 | 280 | BackChannel = function(session, res, query) { 281 | this.res = res; 282 | this.methods = messagingMethods(session.options, query, res); 283 | this.chunk = query.CI === '0'; 284 | this.bytesSent = 0; 285 | this.listener = function() { 286 | session._backChannel.listener = null; 287 | return session._clearBackChannel(res); 288 | }; 289 | }; 290 | 291 | BCSession.prototype._setBackChannel = function(res, query) { 292 | this._clearBackChannel(); 293 | this._backChannel = new BackChannel(this, res, query); 294 | res.connection.once('close', this._backChannel.listener); 295 | this._refreshHeartbeat(); 296 | clearTimeout(this._sessionTimeout); 297 | if (this._outgoingArrays.length > 0) { 298 | this._lastSentArrayId = this._outgoingArrays[0].id - 1; 299 | } 300 | return this.flush(); 301 | }; 302 | 303 | BCSession.prototype._clearBackChannel = function(res) { 304 | if (!this._backChannel) { 305 | return; 306 | } 307 | if ((res != null) && res !== this._backChannel.res) { 308 | return; 309 | } 310 | if (this._backChannel.listener) { 311 | this._backChannel.res.connection.removeListener('close', this._backChannel.listener); 312 | this._backChannel.listener = null; 313 | } 314 | clearTimeout(this._heartbeat); 315 | this._backChannel.methods.end(); 316 | this._backChannel = null; 317 | return this._refreshSessionTimeout(); 318 | }; 319 | 320 | BCSession.prototype._refreshHeartbeat = function() { 321 | var session; 322 | clearTimeout(this._heartbeat); 323 | session = this; 324 | return this._heartbeat = setInterval(function() { 325 | return session.send(['noop']); 326 | }, this.options.keepAliveInterval); 327 | }; 328 | 329 | BCSession.prototype._refreshSessionTimeout = function() { 330 | var session; 331 | clearTimeout(this._sessionTimeout); 332 | session = this; 333 | return this._sessionTimeout = setTimeout(function() { 334 | return session.close('Timed out'); 335 | }, this.options.sessionTimeoutInterval); 336 | }; 337 | 338 | BCSession.prototype._acknowledgeArrays = function(id) { 339 | var confirmcallback; 340 | if (typeof id === 'string') { 341 | id = parseInt(id); 342 | } 343 | while (this._outgoingArrays.length > 0 && this._outgoingArrays[0].id <= id) { 344 | confirmcallback = this._outgoingArrays.shift().confirmcallback; 345 | if (typeof confirmcallback === "function") { 346 | confirmcallback(); 347 | } 348 | } 349 | }; 350 | 351 | OutgoingArray = function(id, data, sendcallback, confirmcallback) { 352 | this.id = id; 353 | this.data = data; 354 | this.sendcallback = sendcallback; 355 | this.confirmcallback = confirmcallback; 356 | }; 357 | 358 | BCSession.prototype._queueArray = function(data, sendcallback, confirmcallback) { 359 | var id; 360 | if (this.state === 'closed') { 361 | return typeof confirmcallback === "function" ? confirmcallback(new Error('closed')) : void 0; 362 | } 363 | id = ++this._lastArrayId; 364 | this._outgoingArrays.push(new OutgoingArray(id, data, sendcallback, confirmcallback)); 365 | return this._lastArrayId; 366 | }; 367 | 368 | BCSession.prototype.send = function(arr, callback) { 369 | var id; 370 | id = this._queueArray(arr, null, callback); 371 | this.flush(); 372 | return id; 373 | }; 374 | 375 | BCSession.prototype._receivedData = function(rid, data) { 376 | var session; 377 | session = this; 378 | return this._ridBuffer(rid, function() { 379 | var id, map, message, _i, _j, _len, _len1, _ref, _ref1; 380 | if (data === null) { 381 | return; 382 | } 383 | if (!((data.maps != null) || (data.json != null))) { 384 | throw new Error('Invalid data'); 385 | } 386 | session._ridBuffer(rid); 387 | id = data.ofs; 388 | if (data.maps) { 389 | _ref = data.maps; 390 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 391 | map = _ref[_i]; 392 | session._mapBuffer(id++, (function(map) { 393 | return function() { 394 | var message; 395 | if (session.state === 'closed') { 396 | return; 397 | } 398 | session.emit('map', map); 399 | if (map.JSON != null) { 400 | try { 401 | message = JSON.parse(map.JSON); 402 | } catch (_error) { 403 | e = _error; 404 | session.close('Invalid JSON'); 405 | return; 406 | } 407 | return session.emit('message', message); 408 | } else if (map._S != null) { 409 | return session.emit('message', map._S); 410 | } 411 | }; 412 | })(map)); 413 | } 414 | } else { 415 | _ref1 = data.json; 416 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 417 | message = _ref1[_j]; 418 | session._mapBuffer(id++, (function(map) { 419 | return function() { 420 | if (session.state === 'closed') { 421 | return; 422 | } 423 | return session.emit('message', message); 424 | }; 425 | })(map)); 426 | } 427 | } 428 | }); 429 | }; 430 | 431 | BCSession.prototype._disconnectAt = function(rid) { 432 | var session; 433 | session = this; 434 | return this._ridBuffer(rid, function() { 435 | return session.close('Disconnected'); 436 | }); 437 | }; 438 | 439 | BCSession.prototype._backChannelStatus = function() { 440 | var a, data, numUnsentArrays, outstandingBytes, unacknowledgedArrays; 441 | numUnsentArrays = this._lastArrayId - this._lastSentArrayId; 442 | unacknowledgedArrays = this._outgoingArrays.slice(0, this._outgoingArrays.length - numUnsentArrays); 443 | outstandingBytes = unacknowledgedArrays.length === 0 ? 0 : (data = (function() { 444 | var _i, _len, _results; 445 | _results = []; 446 | for (_i = 0, _len = unacknowledgedArrays.length; _i < _len; _i++) { 447 | a = unacknowledgedArrays[_i]; 448 | _results.push(a.data); 449 | } 450 | return _results; 451 | })(), JSON.stringify(data).length); 452 | return [(this._backChannel ? 1 : 0), this._lastSentArrayId, outstandingBytes]; 453 | }; 454 | 455 | BCSession.prototype.flush = function() { 456 | var session; 457 | session = this; 458 | return process.nextTick(function() { 459 | return session._flush(); 460 | }); 461 | }; 462 | 463 | BCSession.prototype._flush = function() { 464 | var a, arrays, bytes, data, id, numUnsentArrays, _i, _len; 465 | if (!this._backChannel) { 466 | return; 467 | } 468 | numUnsentArrays = this._lastArrayId - this._lastSentArrayId; 469 | if (numUnsentArrays > 0) { 470 | arrays = this._outgoingArrays.slice(this._outgoingArrays.length - numUnsentArrays); 471 | data = (function() { 472 | var _i, _len, _ref, _results; 473 | _results = []; 474 | for (_i = 0, _len = arrays.length; _i < _len; _i++) { 475 | _ref = arrays[_i], id = _ref.id, data = _ref.data; 476 | _results.push([id, data]); 477 | } 478 | return _results; 479 | })(); 480 | bytes = JSON.stringify(data) + "\n"; 481 | bytes = bytes.replace(/\u2028/g, "\\u2028"); 482 | bytes = bytes.replace(/\u2029/g, "\\u2029"); 483 | this._backChannel.methods.write(bytes); 484 | this._backChannel.bytesSent += bytes.length; 485 | this._lastSentArrayId = this._lastArrayId; 486 | for (_i = 0, _len = arrays.length; _i < _len; _i++) { 487 | a = arrays[_i]; 488 | if (a.sendcallback != null) { 489 | if (typeof a.sendcallback === "function") { 490 | a.sendcallback(); 491 | } 492 | delete a.sendcallback; 493 | } 494 | } 495 | if (this._backChannel && (!this._backChannel.chunk || this._backChannel.bytesSent > 10 * 1024)) { 496 | this._clearBackChannel(); 497 | } 498 | } 499 | if (this.state === 'init') { 500 | return this._changeState('ok'); 501 | } 502 | }; 503 | 504 | BCSession.prototype.stop = function(callback) { 505 | if (this.state === 'closed') { 506 | return; 507 | } 508 | this._queueArray(['stop'], callback, null); 509 | return this.flush(); 510 | }; 511 | 512 | BCSession.prototype.close = function(message) { 513 | var confirmcallback, _i, _len, _ref; 514 | if (this.state === 'closed') { 515 | return; 516 | } 517 | this._changeState('closed'); 518 | this.emit('close', message); 519 | this._clearBackChannel(); 520 | clearTimeout(this._sessionTimeout); 521 | _ref = this._outgoingArrays; 522 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 523 | confirmcallback = _ref[_i].confirmcallback; 524 | if (typeof confirmcallback === "function") { 525 | confirmcallback(new Error(message || 'closed')); 526 | } 527 | } 528 | }; 529 | 530 | module.exports = browserChannel = function(options, onConnect) { 531 | var base, createSession, h, middleware, option, sessions, value, _base, _ref; 532 | if (typeof onConnect === 'undefined') { 533 | onConnect = options; 534 | options = {}; 535 | } 536 | options || (options = {}); 537 | for (option in defaultOptions) { 538 | value = defaultOptions[option]; 539 | if (options[option] == null) { 540 | options[option] = value; 541 | } 542 | } 543 | if (!options.headers) { 544 | options.headers = {}; 545 | } 546 | for (h in standardHeaders) { 547 | v = standardHeaders[h]; 548 | (_base = options.headers)[h] || (_base[h] = v); 549 | } 550 | if (options.cors && typeof options.cors === 'string') { 551 | options.headers['Access-Control-Allow-Origin'] = options.cors; 552 | } 553 | if (options.corsAllowCredentials) { 554 | options.headers['Access-Control-Allow-Credentials'] = true; 555 | } 556 | base = options.base; 557 | if (base.match(/\/$/)) { 558 | base = base.slice(0, base.length - 1); 559 | } 560 | if (base.length > 0 && !base.match(/^\//)) { 561 | base = "/" + base; 562 | } 563 | sessions = {}; 564 | createSession = function(address, query, headers) { 565 | var oldArrayId, oldSession, oldSessionId, session; 566 | oldSessionId = query.OSID, oldArrayId = query.OAID; 567 | if ((oldSessionId != null) && (oldSession = sessions[oldSessionId])) { 568 | oldSession._acknowledgeArrays(oldArrayId); 569 | oldSession.close('Reconnected'); 570 | } 571 | session = new BCSession(address, query, headers, options); 572 | sessions[session.id] = session; 573 | session.on('close', function() { 574 | return delete sessions[session.id]; 575 | }); 576 | return session; 577 | }; 578 | middleware = function(req, res, next) { 579 | var blockedPrefix, dataError, end, etag, headers, hostPrefix, pathname, processData, query, session, write, writeError, writeHead, writeRaw, _ref, _ref1, _ref2, _ref3; 580 | _ref = parse(req.url, true), query = _ref.query, pathname = _ref.pathname; 581 | if (pathname.substring(0, base.length + 1) !== ("" + base + "/")) { 582 | return next(); 583 | } 584 | _ref1 = messagingMethods(options, query, res), writeHead = _ref1.writeHead, write = _ref1.write, writeRaw = _ref1.writeRaw, end = _ref1.end, writeError = _ref1.writeError; 585 | if (pathname === ("" + base + "/bcsocket.js")) { 586 | etag = "\"" + clientStats.size + "-" + (clientStats.mtime.getTime()) + "\""; 587 | res.writeHead(200, 'OK', { 588 | 'Content-Type': 'application/javascript', 589 | 'ETag': etag, 590 | 'Content-Length': clientCode.length 591 | }); 592 | if (req.method === 'HEAD') { 593 | return res.end(); 594 | } else { 595 | return res.end(clientCode); 596 | } 597 | } else if (pathname === ("" + base + "/test")) { 598 | if (query.VER !== '8') { 599 | return sendError(res, 400, 'Version 8 required', options); 600 | } 601 | if (query.MODE === 'init' && req.method === 'GET') { 602 | hostPrefix = getHostPrefix(options); 603 | blockedPrefix = null; 604 | headers = {}; 605 | _ref2 = options.headers; 606 | for (k in _ref2) { 607 | v = _ref2[k]; 608 | headers[k] = v; 609 | } 610 | if (options.cors && typeof options.cors === 'function') { 611 | headers['Access-Control-Allow-Origin'] = options.cors(req, res); 612 | } 613 | headers['X-Accept'] = 'application/json; application/x-www-form-urlencoded'; 614 | res.writeHead(200, 'OK', headers); 615 | return res.end(JSON.stringify([hostPrefix, blockedPrefix])); 616 | } else { 617 | writeHead(); 618 | writeRaw('11111'); 619 | return setTimeout((function() { 620 | writeRaw('2'); 621 | return end(); 622 | }), 2000); 623 | } 624 | } else if (pathname === ("" + base + "/bind")) { 625 | if (query.VER !== '8') { 626 | return sendError(res, 400, 'Version 8 required', options); 627 | } 628 | if (query.SID) { 629 | session = sessions[query.SID]; 630 | if (!session) { 631 | return sendError(res, 400, 'Unknown SID', options); 632 | } 633 | } 634 | if ((query.AID != null) && session) { 635 | session._acknowledgeArrays(query.AID); 636 | } 637 | if (req.method === 'POST') { 638 | if (session === void 0) { 639 | session = createSession(req.connection.remoteAddress, query, req.headers); 640 | if (typeof onConnect === "function") { 641 | onConnect(session, req); 642 | } 643 | session.emit('req', req); 644 | } 645 | dataError = function(e) { 646 | console.warn('Error parsing forward channel', e.stack); 647 | return sendError(res, 400, 'Bad data', options); 648 | }; 649 | processData = function(data) { 650 | var response; 651 | try { 652 | data = transformData(req, data); 653 | session._receivedData(query.RID, data); 654 | } catch (_error) { 655 | e = _error; 656 | return dataError(e); 657 | } 658 | if (session.state === 'init') { 659 | res.writeHead(200, 'OK', options.headers); 660 | session._setBackChannel(res, { 661 | CI: 1, 662 | TYPE: 'xmlhttp', 663 | RID: 'rpc' 664 | }); 665 | return session.flush(); 666 | } else if (session.state === 'closed') { 667 | return sendError(res, 403, 'Forbidden', options); 668 | } else { 669 | response = JSON.stringify(session._backChannelStatus()); 670 | res.writeHead(200, 'OK', options.headers); 671 | return res.end("" + response.length + "\n" + response); 672 | } 673 | }; 674 | if (req.body) { 675 | return processData(req.body); 676 | } else { 677 | return bufferPostData(req, function(data) { 678 | try { 679 | data = decodeData(req, data); 680 | } catch (_error) { 681 | e = _error; 682 | return dataError(e); 683 | } 684 | return processData(data); 685 | }); 686 | } 687 | } else if (req.method === 'GET') { 688 | if ((_ref3 = query.TYPE) === 'xmlhttp' || _ref3 === 'html') { 689 | if (typeof query.SID !== 'string' && query.SID.length < 5) { 690 | return sendError(res, 400, 'Invalid SID', options); 691 | } 692 | if (query.RID !== 'rpc') { 693 | return sendError(res, 400, 'Expected RPC', options); 694 | } 695 | writeHead(); 696 | return session._setBackChannel(res, query); 697 | } else if (query.TYPE === 'terminate') { 698 | if (session != null) { 699 | session._disconnectAt(query.RID); 700 | } 701 | res.writeHead(200, 'OK', options.headers); 702 | return res.end(); 703 | } 704 | } else { 705 | res.writeHead(405, 'Method Not Allowed', options.headers); 706 | return res.end("Method not allowed"); 707 | } 708 | } else { 709 | res.writeHead(404, 'Not Found', options.headers); 710 | return res.end("Not found"); 711 | } 712 | }; 713 | middleware.close = function() { 714 | var id, session; 715 | for (id in sessions) { 716 | session = sessions[id]; 717 | session.close(); 718 | } 719 | }; 720 | if ((_ref = options.server) != null) { 721 | _ref.on('close', middleware.close); 722 | } 723 | return middleware; 724 | }; 725 | 726 | browserChannel._setTimerMethods = function(methods) { 727 | return setInterval = methods.setInterval, clearInterval = methods.clearInterval, setTimeout = methods.setTimeout, clearTimeout = methods.clearTimeout, Date = methods.Date, methods; 728 | }; 729 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | body { 3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 4 | font-size: 15px; 5 | line-height: 22px; 6 | color: #252519; 7 | margin: 0; padding: 0; 8 | } 9 | a { 10 | color: #261a3b; 11 | } 12 | a:visited { 13 | color: #261a3b; 14 | } 15 | p { 16 | margin: 0 0 15px 0; 17 | } 18 | h1, h2, h3, h4, h5, h6 { 19 | margin: 0px 0 15px 0; 20 | } 21 | h1 { 22 | margin-top: 40px; 23 | } 24 | #container { 25 | position: relative; 26 | } 27 | #background { 28 | position: fixed; 29 | top: 0; left: 525px; right: 0; bottom: 0; 30 | background: #f5f5ff; 31 | border-left: 1px solid #e5e5ee; 32 | z-index: -1; 33 | } 34 | #jump_to, #jump_page { 35 | background: white; 36 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 37 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 38 | font: 10px Arial; 39 | text-transform: uppercase; 40 | cursor: pointer; 41 | text-align: right; 42 | } 43 | #jump_to, #jump_wrapper { 44 | position: fixed; 45 | right: 0; top: 0; 46 | padding: 5px 10px; 47 | } 48 | #jump_wrapper { 49 | padding: 0; 50 | display: none; 51 | } 52 | #jump_to:hover #jump_wrapper { 53 | display: block; 54 | } 55 | #jump_page { 56 | padding: 5px 0 3px; 57 | margin: 0 0 25px 25px; 58 | } 59 | #jump_page .source { 60 | display: block; 61 | padding: 5px 10px; 62 | text-decoration: none; 63 | border-top: 1px solid #eee; 64 | } 65 | #jump_page .source:hover { 66 | background: #f5f5ff; 67 | } 68 | #jump_page .source:first-child { 69 | } 70 | table td { 71 | border: 0; 72 | outline: 0; 73 | } 74 | td.docs, th.docs { 75 | max-width: 450px; 76 | min-width: 450px; 77 | min-height: 5px; 78 | padding: 10px 25px 1px 50px; 79 | overflow-x: hidden; 80 | vertical-align: top; 81 | text-align: left; 82 | } 83 | .docs pre { 84 | margin: 15px 0 15px; 85 | padding-left: 15px; 86 | } 87 | .docs p tt, .docs p code { 88 | background: #f8f8ff; 89 | border: 1px solid #dedede; 90 | font-size: 12px; 91 | padding: 0 0.2em; 92 | } 93 | .pilwrap { 94 | position: relative; 95 | } 96 | .pilcrow { 97 | font: 12px Arial; 98 | text-decoration: none; 99 | color: #454545; 100 | position: absolute; 101 | top: 3px; left: -20px; 102 | padding: 1px 2px; 103 | opacity: 0; 104 | -webkit-transition: opacity 0.2s linear; 105 | } 106 | td.docs:hover .pilcrow { 107 | opacity: 1; 108 | } 109 | td.code, th.code { 110 | padding: 14px 15px 16px 25px; 111 | width: 100%; 112 | vertical-align: top; 113 | background: #f5f5ff; 114 | border-left: 1px solid #e5e5ee; 115 | } 116 | pre, tt, code { 117 | font-size: 12px; line-height: 18px; 118 | font-family: Monaco, Consolas, "Lucida Console", monospace; 119 | margin: 0; padding: 0; 120 | } 121 | 122 | 123 | /*---------------------- Syntax Highlighting -----------------------------*/ 124 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 125 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 126 | body .hll { background-color: #ffffcc } 127 | body .c { color: #408080; font-style: italic } /* Comment */ 128 | body .err { border: 1px solid #FF0000 } /* Error */ 129 | body .k { color: #954121 } /* Keyword */ 130 | body .o { color: #666666 } /* Operator */ 131 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 132 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 133 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 134 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 135 | body .gd { color: #A00000 } /* Generic.Deleted */ 136 | body .ge { font-style: italic } /* Generic.Emph */ 137 | body .gr { color: #FF0000 } /* Generic.Error */ 138 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 139 | body .gi { color: #00A000 } /* Generic.Inserted */ 140 | body .go { color: #808080 } /* Generic.Output */ 141 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 142 | body .gs { font-weight: bold } /* Generic.Strong */ 143 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 144 | body .gt { color: #0040D0 } /* Generic.Traceback */ 145 | body .kc { color: #954121 } /* Keyword.Constant */ 146 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ 147 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ 148 | body .kp { color: #954121 } /* Keyword.Pseudo */ 149 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 150 | body .kt { color: #B00040 } /* Keyword.Type */ 151 | body .m { color: #666666 } /* Literal.Number */ 152 | body .s { color: #219161 } /* Literal.String */ 153 | body .na { color: #7D9029 } /* Name.Attribute */ 154 | body .nb { color: #954121 } /* Name.Builtin */ 155 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 156 | body .no { color: #880000 } /* Name.Constant */ 157 | body .nd { color: #AA22FF } /* Name.Decorator */ 158 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 159 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 160 | body .nf { color: #0000FF } /* Name.Function */ 161 | body .nl { color: #A0A000 } /* Name.Label */ 162 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 163 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 164 | body .nv { color: #19469D } /* Name.Variable */ 165 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 166 | body .w { color: #bbbbbb } /* Text.Whitespace */ 167 | body .mf { color: #666666 } /* Literal.Number.Float */ 168 | body .mh { color: #666666 } /* Literal.Number.Hex */ 169 | body .mi { color: #666666 } /* Literal.Number.Integer */ 170 | body .mo { color: #666666 } /* Literal.Number.Oct */ 171 | body .sb { color: #219161 } /* Literal.String.Backtick */ 172 | body .sc { color: #219161 } /* Literal.String.Char */ 173 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 174 | body .s2 { color: #219161 } /* Literal.String.Double */ 175 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 176 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 177 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 178 | body .sx { color: #954121 } /* Literal.String.Other */ 179 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 180 | body .s1 { color: #219161 } /* Literal.String.Single */ 181 | body .ss { color: #19469D } /* Literal.String.Symbol */ 182 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 183 | body .vc { color: #19469D } /* Name.Variable.Class */ 184 | body .vg { color: #19469D } /* Name.Variable.Global */ 185 | body .vi { color: #19469D } /* Name.Variable.Instance */ 186 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/test.html: -------------------------------------------------------------------------------- 1 | test.coffee

test.coffee

connect = require 'connect'
 2 | browserchannel = require('./lib').server
 3 | 
 4 | server = connect(
 5 | 	connect.favicon()
 6 | 	connect.static "#{__dirname}/public"
 7 | 	connect.logger()
 8 | 	browserchannel('/channel')
 9 | ).listen 4321
10 | console.log 'Running on localhost:4321'
11 | 
12 | 
-------------------------------------------------------------------------------- /examples/chatserver.coffee: -------------------------------------------------------------------------------- 1 | browserChannel = require('browserchannel').server 2 | connect = require 'connect' 3 | 4 | clients = [] 5 | 6 | server = connect( 7 | connect.static "#{__dirname}/public" 8 | browserChannel (client) -> 9 | console.log "Client #{client.id} connected" 10 | 11 | clients.push client 12 | 13 | client.on 'map', (data) -> 14 | console.log "#{client.id} sent #{JSON.stringify data}" 15 | # broadcast to all other clients 16 | c.send data for c in clients when c != client 17 | 18 | client.on 'close', (reason) -> 19 | console.log "Client #{client.id} disconnected (#{reason})" 20 | # Remove the client from the client list 21 | clients = (c for c in clients when c != client) 22 | 23 | ).listen(4321) 24 | 25 | console.log 'Echo server listening on localhost:4321' 26 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Joseph Gentle (http://josephg.com/)", 3 | "name": "browserchannel-chatserver-demo", 4 | "description": "Google BrowserChannel chatserver demo for NodeJS", 5 | "homepage": "https://github.com/josephg/node-browserchannel", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/josephg/node-browserchannel.git" 9 | }, 10 | "main": "chatserver.coffee", 11 | "engines": { 12 | "node": "0.10.x" 13 | }, 14 | "scripts": { 15 | "start": "coffee chatserver.coffee" 16 | }, 17 | "dependencies": { 18 | "browserchannel": "../", 19 | "connect": "~1", 20 | "coffee-script": "~1.7" 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.server = require('./dist/server'); 2 | 3 | // This exposes the bare browserchannel implementation. 4 | //exports.goog = require('./dist/node-browserchannel.js'); 5 | 6 | var BCSocket = require('./dist/node-bcsocket-uncompressed.js'); 7 | exports.BCSocket = BCSocket.BCSocket; 8 | exports.setDefaultLocation = BCSocket.setDefaultLocation; 9 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | Most of the files in here are built by the closure compiler. 2 | 3 | They are assembled by running 4 | 5 | % cake client 6 | -------------------------------------------------------------------------------- /lib/bcsocket.coffee: -------------------------------------------------------------------------------- 1 | # This is a little wrapper around browserchannels which exposes something thats compatible 2 | # with the websocket API. It also supports automatic reconnecting, and some other goodies. 3 | # 4 | # You can use it just like websockets: 5 | # 6 | # var socket = new BCSocket '/foo' 7 | # socket.onopen = -> 8 | # socket.send 'hi mum!' 9 | # socket.onmessage = (message) -> 10 | # console.log 'got message', message 11 | # 12 | # ... etc. See here for specs: 13 | # http://dev.w3.org/html5/websockets/ 14 | # 15 | # I've also added: 16 | # 17 | # - You can reconnect a disconnected socket using .open(). 18 | # - .send() transparently works with JSON objects. 19 | # - .sendMap() works as a lower level sending mechanism. 20 | # - The second argument can be an options argument. Valid options: 21 | # - **appVersion**: Your application's protocol version. This is passed to the server-side 22 | # browserchannel code, in through your session handler as `session.appVersion` 23 | # - **prev**: The previous BCSocket object, if one exists. When the socket is established, 24 | # the previous bcsocket session will be disconnected as we reconnect. 25 | # - **reconnect**: Tell the socket to automatically reconnect when its been disconnected. 26 | # - **failFast**: Make the socket report errors immediately, rather than trying a few times 27 | # first. 28 | # - **crossDomainXhr**: Set to true to enable the cross-origin credential 29 | # flags in XHR requests. The server must send the 30 | # Access-Control-Allow-Credentials header and can't use wildcard access 31 | # control hostnames. This is needed if you're using host prefixes. See: 32 | # http://www.html5rocks.com/en/tutorials/cors/#toc-withcredentials 33 | # - **extraParams**: Extra query parameters to be sent with requests. If 34 | # present, this should be a map of query parameter / value pairs. Note that 35 | # these parameters are resent with every request, so you might want to think 36 | # twice before putting a lot of stuff in here. 37 | # - **extraHeaders**: Extra headers to add to requests. Be advised that not 38 | # all headers are allowed by the XHR spec. Headers from NodeJS clients are 39 | # unrestricted. 40 | 41 | goog.provide 'bc.BCSocket' 42 | 43 | goog.require 'goog.net.BrowserChannel' 44 | goog.require 'goog.net.BrowserChannel.Handler' 45 | goog.require 'goog.net.BrowserChannel.Error' 46 | goog.require 'goog.net.BrowserChannel.State' 47 | goog.require 'goog.string' 48 | 49 | # Uncomment for extra debugging information in the console. This currently breaks nodejs support 50 | # unfortunately. 51 | # 52 | # goog.require 'goog.debug.Console' 53 | # goog.debug.Console.instance = new goog.debug.Console() 54 | # goog.debug.Console.instance.setCapturing(true 55 | 56 | # Closure uses numerical error codes. We'll translate them into strings for the user. 57 | errorMessages = {} 58 | errorMessages[goog.net.BrowserChannel.Error.OK] = 'Ok' 59 | errorMessages[goog.net.BrowserChannel.Error.LOGGED_OUT] = 'User is logging out' 60 | errorMessages[goog.net.BrowserChannel.Error.UNKNOWN_SESSION_ID] = 'Unknown session ID' 61 | errorMessages[goog.net.BrowserChannel.Error.STOP] = 'Stopped by server' 62 | 63 | # All of these error messages basically boil down to "Something went wrong - try again". I can't 64 | # imagine using different logic on the client based on the error here - just keep reconnecting. 65 | 66 | # The client's internet is down (ping to google failed) 67 | errorMessages[goog.net.BrowserChannel.Error.NETWORK] = 'General network error' 68 | # The server could not be contacted 69 | errorMessages[goog.net.BrowserChannel.Error.REQUEST_FAILED] = 'Request failed' 70 | 71 | # This error happens when the client can't connect to the special test domain. In my experience, 72 | # this error happens normally sometimes as well - if one particular connection doesn't 73 | # make it through during the channel test. This will never happen with node-browserchannel anyway 74 | # because we don't support the network admin blocking channel. 75 | errorMessages[goog.net.BrowserChannel.Error.BLOCKED] = 'Blocked by a network administrator' 76 | 77 | # We got an invalid response from the server 78 | errorMessages[goog.net.BrowserChannel.Error.NO_DATA] = 'No data from server' 79 | errorMessages[goog.net.BrowserChannel.Error.BAD_DATA] = 'Got bad data from the server' 80 | errorMessages[goog.net.BrowserChannel.Error.BAD_RESPONSE] = 'Got a bad response from the server' 81 | 82 | `/** @constructor */` 83 | BCSocket = (url, options) -> 84 | return new BCSocket(url, options) unless this instanceof BCSocket 85 | 86 | self = this 87 | 88 | # Url can be relative or absolute. (Though an absolute URL in the browser will 89 | # have to match same origin policy) 90 | url ||= 'channel' 91 | 92 | # Websocket urls are specified as ws:// or wss://. Replace the leading ws with 93 | # http. 94 | url.replace /^ws/, 'http' if url.match /:\/\// 95 | 96 | options ||= {} 97 | 98 | # Using websockets you can specify an array of protocol versions or a protocol 99 | # version string. All that stuff is ignored. 100 | options = {} if goog.isArray options or typeof options is 'string' 101 | 102 | reconnectTime = options['reconnectTime'] or 3000 103 | 104 | # Extra headers. Not all headers can be set, and the headers that can be set 105 | # changes depending on whether we're connecting from nodejs or from the 106 | # browser. 107 | extraHeaders = options['extraHeaders'] or null 108 | 109 | # Extra GET parameters 110 | extraParams = options['extraParams'] or null 111 | 112 | # Generate a session affinity token to send with all requests. 113 | # For use with a load balancer that parses GET variables. 114 | unless options['affinity'] is null 115 | extraParams ||= {} 116 | options['affinityParam'] ||= 'a' 117 | 118 | @['affinity'] = options['affinity'] || goog.string.getRandomString() 119 | extraParams[options['affinityParam']] = @['affinity'] 120 | 121 | # The channel starts CLOSED. When connect() is called, the channel moves into 122 | # the CONNECTING state. If it connects, it moves to OPEN. If an error occurs 123 | # (or an error occurs while the connection is connected), the socket moves to 124 | # 'CLOSED' again. 125 | # 126 | # At any time, you can call close(), which disconnects the socket. 127 | 128 | setState = (state) -> # This is convenient for logging state changes, and increases compression. 129 | #console?.log "state from #{self.readyState} to #{state}" 130 | self.readyState = self['readyState'] = state 131 | 132 | setState @CLOSED 133 | 134 | # The current browserchannel session we're connected through. 135 | session = null 136 | 137 | # When we reconnect, we'll pass the SID and AID from the previous time we 138 | # successfully connected. This ghosts the previous session if the server 139 | # thinks its still around. If you aren't using BCSocket's reconnection 140 | # support, pass the old BCSocket object in to options.prev. 141 | # 142 | # It might be more correct here to use lastSession instead (which would mean 143 | # we would only use a session after it has been opened). 144 | lastSession = options['prev']?.session 145 | 146 | # Closure has an annoyingly complicated logging system which by default will 147 | # silently capture & discard any errors thrown in callbacks. I could enable 148 | # the logging infrastructure (above), but I prefer to just log errors as 149 | # needed. 150 | # 151 | # The callback takes three arguments because thats the max any event needs to 152 | # pass into its callback. 153 | fireCallback = (name, shouldThrow, a, b, c) -> 154 | try 155 | self[name]? a, b, c 156 | catch e 157 | console?.error e.stack 158 | throw e 159 | 160 | # A handler is used to receive events back out of the session. 161 | handler = new goog.net.BrowserChannel.Handler() 162 | 163 | handler.channelOpened = (channel) -> 164 | lastSession = session 165 | setState BCSocket.OPEN 166 | fireCallback 'onopen', true 167 | 168 | # If there's an error, the handler's channelError() method is called right 169 | # before channelClosed(). We'll cache the error so a 'disconnect' handler 170 | # knows the disconnect reason. 171 | lastErrorCode = null 172 | 173 | # This is called when the session has the final error explaining why its 174 | # closing. It is called only once, just before channelClosed(). It is not 175 | # called if the session is manually disconnected. 176 | handler.channelError = (channel, errCode) -> 177 | message = errorMessages[errCode] 178 | #console?.error "channelError #{errCode} : #{message} in state #{self.readyState}" 179 | lastErrorCode = errCode 180 | 181 | # If your network connection is down, you'll get General Network Errors 182 | # passing through here even when you're not connected. 183 | setState BCSocket.CLOSING unless self.readyState is BCSocket.CLOSED 184 | 185 | # I'm not 100% sure what websockets do if there's an error like this. I'm 186 | # going to assume it has the same behaviour as browserchannel - that is, 187 | # onclose() is always called if a connection closes, and onerror is called 188 | # whenever an error occurs. 189 | 190 | # If fireCallback throws, channelClosed (below) never gets called, which in 191 | # turn causes the connection to never reconnect. We'll eat the exceptions so 192 | # that doesn't happen. 193 | fireCallback 'onerror', false, message, errCode 194 | 195 | # When HTTP connection with session goes down because of network errors, 196 | # handler uses this URL to make and HTTP request. If it succeeds, handler 197 | # understands, that network is ok, it's just backend went down. 198 | # It if does not succeed, user has probably disconnected from network at all. 199 | # the URL should point to a tiny image. 200 | handler.getNetworkTestImageUri = (obj) -> 201 | return options['testImageUri'] 202 | 203 | reconnectTimer = null 204 | 205 | # This will be called whenever the client disconnects or fails to connect for 206 | # any reason. When we fail to connect, I'll also fire 'onclose' (even though 207 | # onopen is never called!) for two reasons: 208 | # 209 | # - The state machine goes from CLOSED -> CONNECTING -> CLOSING -> CLOSED, so 210 | # technically we did enter the 'close' state. 211 | # - Thats what websockets do (onclose() is called on a websocket if it fails 212 | # to connect). 213 | handler.channelClosed = (channel, pendingMaps, undeliveredMaps) -> 214 | #console.trace 'channelClosed ' + self.readyState 215 | 216 | # Hm. 217 | # 218 | # I'm not sure what to do with this potentially-undelivered data. I think 219 | # I'll toss it to the emitter and let that deal with it. 220 | # 221 | # I'd rather call a callback on send(), like the server does. But I can't, 222 | # because browserchannel's API isn't rich enough. 223 | 224 | # Should handle server stop 225 | return if self.readyState is BCSocket.CLOSED 226 | 227 | # And once channelClosed is called, we won't get any more events from the 228 | # session. So things like send() should throw exceptions. 229 | session = null 230 | 231 | message = if lastErrorCode then errorMessages[lastErrorCode] else 'Closed' 232 | 233 | setState BCSocket.CLOSED 234 | 235 | # If the error message is STOP, we won't reconnect. That means the server 236 | # has explicitly requested the client give up trying to reconnect due to 237 | # some error. 238 | # 239 | # The error code will be 'OK' if close() was called on the client. 240 | if options['reconnect'] and lastErrorCode not in [goog.net.BrowserChannel.Error.STOP, goog.net.BrowserChannel.Error.OK] 241 | #console.warn 'rc' 242 | # If the session ID is unknown, that means the session has timed out. We 243 | # can reconnect immediately. 244 | time = if lastErrorCode is goog.net.BrowserChannel.Error.UNKNOWN_SESSION_ID then 0 else reconnectTime 245 | 246 | clearTimeout reconnectTimer 247 | reconnectTimer = setTimeout reconnect, time 248 | 249 | # This happens after the reconnect timer is set so the callback can call 250 | # close() to cancel reconnection. 251 | fireCallback 'onclose', false, message, pendingMaps, undeliveredMaps 252 | 253 | # make sure we don't reuse an old error message later 254 | lastErrorCode = null 255 | 256 | # Messages from the server are passed directly. 257 | handler.channelHandleArray = (channel, data) -> 258 | # Websocket onmessage handlers accept a MessageEvent object, which contains 259 | # all sorts of other stuff unrelated to the message itself. 260 | message = 261 | type: 'message' 262 | data: data 263 | 264 | fireCallback 'onmessage', true, message 265 | 266 | # This reconnects if the current session is null. 267 | reconnect = -> 268 | # It should be impossible for this function to be reentrant - the only 269 | # places it can be called from are open() below and from the setTimeout 270 | # above (which is disabled when reconnect is called). I'll just check it 271 | # anyway though, because its sort of important. 272 | throw new Error 'Reconnect() called from invalid state' if session 273 | 274 | setState BCSocket.CONNECTING 275 | fireCallback 'onconnecting', true 276 | 277 | clearTimeout reconnectTimer 278 | 279 | self.session = session = new goog.net.BrowserChannel options['appVersion'], lastSession?.getFirstTestResults() 280 | session.setSupportsCrossDomainXhrs true if options['crossDomainXhr'] 281 | session.setHandler handler 282 | session.setExtraHeaders extraHeaders if extraHeaders 283 | lastErrorCode = null 284 | 285 | session.setFailFast yes if options['failFast'] 286 | 287 | # Only needed for debugging.. 288 | #session.setChannelDebug(new goog.net.ChannelDebug()) 289 | 290 | session.connect "#{url}/test", "#{url}/bind", extraParams, 291 | lastSession?.getSessionId(), lastSession?.getLastArrayId() 292 | 293 | # This isn't in the normal websocket interface. It reopens a previously closed 294 | # websocket connection by reconnecting. 295 | @['open'] = -> 296 | # If the session is already open, you should call close() first. 297 | throw new Error 'Already open' unless self.readyState is self.CLOSED 298 | reconnect() 299 | 300 | # This closes the connection and stops it from reconnecting. 301 | @['close'] = -> 302 | clearTimeout reconnectTimer 303 | 304 | # I'm abusing lastErrorCode here so in the channelClosed handler I can make 305 | # sure we don't try to reconnect. 306 | lastErrorCode = goog.net.BrowserChannel.Error.OK 307 | 308 | return if self.readyState is BCSocket.CLOSED 309 | 310 | setState BCSocket.CLOSING 311 | 312 | # In theory, we don't transition to the CLOSED state until the server has 313 | # received the disconnect message. But in practice, disconnect() results in 314 | # channelClosed() being called immediately. The server is still notified, 315 | # but only really as an afterthought. 316 | session.disconnect() 317 | 318 | # TODO: Make @send to take a callback which is called when the message is 319 | # either confirmed received or failed. The closure library has recently added 320 | # a mechanism to do this. 321 | # 322 | # Note that you *can* send messages while the channel is connecting. Thats a 323 | # *GOOD IDEA* - any messages sent then should be sent with the initial 324 | # payload. 325 | @['sendMap'] = sendMap = (map) -> 326 | # This is the raw way to send messages. We'll silently consume messages sent 327 | # after the connection closes. This is the logic all consumers of the API 328 | # end up implementing anyway. 329 | if self.readyState in [BCSocket.CLOSING, BCSocket.CLOSED] 330 | #console?.warn 'Cannot send to a closed connection' 331 | return 332 | 333 | session.sendMap map 334 | 335 | # This sends a map of {JSON:"..."} or {_S:"..."}. It is interpreted as a native message by the server. 336 | @['send'] = (message) -> 337 | if typeof message is 'string' 338 | sendMap '_S': message 339 | else 340 | sendMap 'JSON': goog.json.serialize message 341 | 342 | # Websocket connections are automatically opened. 343 | reconnect() 344 | 345 | return 346 | 347 | # Flag to tell clients they can cheat and send while the session is being 348 | # established. Its good practice with browserchannel to send messages while 349 | # the session is being set up - its faster for your users. But websockets 350 | # don't support that. We could pretend that connections open immediately (for 351 | # api compatibility), but if people bound UI to the connection state, it would 352 | # look wrong. 353 | # 354 | # If you want a fast start, look for this flag. 355 | BCSocket.prototype['canSendWhileConnecting'] = BCSocket['canSendWhileConnecting'] = true 356 | 357 | # Flag to indicate native JSON support. The advantage of using browserchannel's 358 | # own JSON support is that it uses the closure library's JSON.stringify / 359 | # JSON.parse shims. These shims support old browsers. 360 | BCSocket.prototype['canSendJSON'] = BCSocket['canSendJSON'] = true 361 | 362 | BCSocket.prototype['CONNECTING'] = BCSocket['CONNECTING'] = BCSocket.CONNECTING = 0 363 | BCSocket.prototype['OPEN'] = BCSocket['OPEN'] = BCSocket.OPEN = 1 364 | BCSocket.prototype['CLOSING'] = BCSocket['CLOSING'] = BCSocket.CLOSING = 2 365 | BCSocket.prototype['CLOSED'] = BCSocket['CLOSED'] = BCSocket.CLOSED = 3 366 | 367 | (exports ? window)['BCSocket'] = BCSocket 368 | -------------------------------------------------------------------------------- /lib/browserchannel.coffee: -------------------------------------------------------------------------------- 1 | # These aren't required if we're pulling in the browserchannel code manually. 2 | 3 | goog.provide 'bc' 4 | 5 | goog.require 'goog.net.BrowserChannel' 6 | goog.require 'goog.net.BrowserChannel.Handler' 7 | goog.require 'goog.net.BrowserChannel.Error' 8 | goog.require 'goog.net.BrowserChannel.State' 9 | 10 | p = goog.net.BrowserChannel.prototype 11 | 12 | p['getChannelDebug'] = p.getChannelDebug 13 | p['setChannelDebug'] = p.setChannelDebug 14 | 15 | p['connect'] = p.connect 16 | p['disconnect'] = p.disconnect 17 | p['getSessionId'] = p.getSessionId 18 | 19 | p['getExtraHeaders'] = p.getExtraHeaders 20 | p['setExtraHeaders'] = p.setExtraHeaders 21 | 22 | p['getHandler'] = p.getHandler 23 | p['setHandler'] = p.setHandler 24 | 25 | p['getAllowHostPrefix'] = p.getAllowHostPrefix 26 | p['setAllowHostPrefix'] = p.setAllowHostPrefix 27 | 28 | p['getAllowChunkedMode'] = p.getAllowChunkedMode 29 | p['setAllowChunkedMode'] = p.setAllowChunkedMode 30 | 31 | p['isBuffered'] = p.isBuffered 32 | p['sendMap'] = p.sendMap 33 | 34 | p['setFailFast'] = p.setFailFast 35 | p['isClosed'] = p.isClosed 36 | p['getState'] = p.getState 37 | 38 | p['getLastStatusCode'] = p.getLastStatusCode 39 | 40 | # Not exposed: 41 | # getForwardChannelMaxRetries 42 | # getBackChannelMaxRetries 43 | # getLastArrayId 44 | # hasOutstandingRequests 45 | # shouldUseSecondaryDomains 46 | # 47 | # I think most of these are only supposed to be used by channel request. 48 | 49 | 50 | # We'll use closure's JSON serializer because IE doesn't come with a JSON serializer / parser. 51 | goog.require 'goog.json' 52 | 53 | # Add a little extra extension for node-browserchannel, for sending JSON data. The browserchannel server 54 | # interprets {JSON:...} maps specially and decodes them automatically. 55 | p['send'] = (message) -> 56 | @sendMap 'JSON': goog.json.serialize message 57 | 58 | goog.net.BrowserChannel['Handler'] = goog.net.BrowserChannel.Handler 59 | 60 | # I previously set up aliases for goog.net.BrowserChannel.[Error, State] but the 61 | # closure seems to produce better code if you don't do that. 62 | goog.net.BrowserChannel['Error'] = 63 | 'OK': goog.net.BrowserChannel.Error.OK 64 | 'REQUEST_FAILED': goog.net.BrowserChannel.Error.REQUEST_FAILED 65 | 'LOGGED_OUT': goog.net.BrowserChannel.Error.LOGGED_OUT 66 | 'NO_DATA': goog.net.BrowserChannel.Error.NO_DATA 67 | 'UNKNOWN_SESSION_ID': goog.net.BrowserChannel.Error.UNKNOWN_SESSION_ID 68 | 'STOP': goog.net.BrowserChannel.Error.STOP 69 | 'NETWORK': goog.net.BrowserChannel.Error.NETWORK 70 | 'BLOCKED': goog.net.BrowserChannel.Error.BLOCKED 71 | 'BAD_DATA': goog.net.BrowserChannel.Error.BAD_DATA 72 | 'BAD_RESPONSE': goog.net.BrowserChannel.Error.BAD_RESPONSE 73 | 'ACTIVE_X_BLOCKED': goog.net.BrowserChannel.Error.ACTIVE_X_BLOCKED 74 | 75 | goog.net.BrowserChannel['State'] = 76 | 'CLOSED': goog.net.BrowserChannel.State.CLOSED 77 | 'INIT': goog.net.BrowserChannel.State.INIT 78 | 'OPENING': goog.net.BrowserChannel.State.OPENING 79 | 'OPENED': goog.net.BrowserChannel.State.OPENED 80 | 81 | goog.exportSymbol 'goog.net.BrowserChannel', goog.net.BrowserChannel, exports ? window 82 | -------------------------------------------------------------------------------- /lib/debug.coffee: -------------------------------------------------------------------------------- 1 | # This enables printing massive amounts of debugging messages to the console. 2 | 3 | goog.require 'goog.debug.Logger' 4 | 5 | #logger = goog.debug.Logger.getLogger 'goog.net.BrowserChannel' 6 | logger = goog.debug.Logger.getLogger 'goog.net' 7 | logger.setLevel goog.debug.Logger.Level.FINER 8 | logger.addHandler (msg) -> console.log msg.getMessage() 9 | -------------------------------------------------------------------------------- /lib/handler-externs.js: -------------------------------------------------------------------------------- 1 | // This tells the closure compiler to not munge the names of any of the handler's 2 | // methods. 3 | 4 | /** @constructor */ 5 | var Handler = function(){}; 6 | 7 | Handler.prototype.channelHandleMultipleArrays = null; 8 | Handler.prototype.okToMakeRequest = function(){}; 9 | Handler.prototype.channelOpened = function(){}; 10 | Handler.prototype.channelHandleArray = function(){}; 11 | Handler.prototype.channelError = function(){}; 12 | Handler.prototype.channelClosed = function(){}; 13 | Handler.prototype.getAdditionalParams = function(){}; 14 | Handler.prototype.getNetworkTestImageUri = function(){}; 15 | Handler.prototype.isActive = function(){}; 16 | Handler.prototype.badMapError = function(){}; 17 | Handler.prototype.correctHostPrefix = function(){}; 18 | 19 | -------------------------------------------------------------------------------- /lib/nodejs-override.coffee: -------------------------------------------------------------------------------- 1 | # This fixes some bits and pieces so the browserchannel client works from nodejs. 2 | # 3 | # It is included after browserchannel when you closure compile node-browserchannel.js. 4 | # This is closure compiled. 5 | 6 | # For certain classes of error, browserchannel tries to get a test image from google.com 7 | # to check if the connection is still live. 8 | # 9 | # It also uses an image object with a particular URL when you call disconnect(), to tell 10 | # the server that the connection has gone away. 11 | # 12 | # Its kinda clever, really. 13 | 14 | goog.provide 'bc.node' 15 | 16 | #goog.require 'bc' 17 | 18 | request = require 'request' 19 | 20 | Image = -> 21 | @__defineSetter__ 'src', (url) => 22 | url = url.toString() 23 | if url.match /^\/\// 24 | url = 'http:' + url 25 | 26 | request url, (error, response, body) => 27 | if error? 28 | @onerror?() 29 | else 30 | @onload?() 31 | 32 | this 33 | 34 | # Create XHR objects using the nodejs xmlhttprequest library. 35 | {XMLHttpRequest} = require '../XMLHttpRequest' 36 | 37 | goog.require 'goog.net.XmlHttpFactory' 38 | 39 | goog.net.BrowserChannel.prototype.createXhrIo = (hostPrefix) -> 40 | xhrio = new goog.net.XhrIo() 41 | xhrio.createXhr = -> new XMLHttpRequest() 42 | xhrio 43 | 44 | # If you specify a relative test / bind path, browserchannel interprets it using window.location. 45 | # I'll override that using a local window object with a fake location. 46 | # 47 | # Luckily, nodejs's url.parse module creates an object which is compatible with window.location. 48 | 49 | window = {setTimeout, clearTimeout, setInterval, clearInterval, console} 50 | window.location = null 51 | window.navigator = 52 | userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.202 Safari/535.1" 53 | 54 | ### 55 | setTimeout: (f, t) -> 56 | console.log 'setTimeout' 57 | setTimeout (-> console.log(f.toString()); f()), t 58 | clearTimeout: clearTimeout 59 | setInterval: (f, t) -> 60 | console.log 'setTimeout' 61 | setInterval (-> console.log 'tick'; f()), t 62 | clearInterval: clearInterval 63 | ### 64 | 65 | # This makes closure functions able to access setTimeout / setInterval. I don't know 66 | # why they don't just access them directly, but thats closure for you. 67 | goog.global = window 68 | 69 | url = require 'url' 70 | 71 | # Closure would scramble this name if we didn't specify it in quotes. 72 | exports['setDefaultLocation'] = (loc) -> 73 | if typeof loc is 'string' 74 | loc = url.parse loc 75 | 76 | window.location = loc 77 | 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Joseph Gentle (http://josephg.com/)", 3 | "name": "browserchannel", 4 | "description": "Google BrowserChannel server for NodeJS", 5 | "version": "2.1.0", 6 | "homepage": "https://github.com/josephg/node-browserchannel", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/josephg/node-browserchannel.git" 10 | }, 11 | "main": "index.js", 12 | "scripts": { 13 | "test": "mocha", 14 | "prepublish": "make dist/server.js" 15 | }, 16 | "dependencies": { 17 | "hat": "*", 18 | "request": "~2", 19 | "ascii-json": "~0.2" 20 | }, 21 | "devDependencies": { 22 | "coffee-script": "~1.7", 23 | "timerstub": "*", 24 | "mocha": "~1.18", 25 | "express": "^4.6.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/bcsocket.coffee: -------------------------------------------------------------------------------- 1 | # # Tests for the bare BrowserChannel client. 2 | # 3 | # Run them by first launching 4 | # 5 | # % coffee test/support/runserver.coffee 6 | # 7 | # ... Then browsing to localhost:4321 in your browser or running: 8 | # 9 | # % mocha test/bcsocket.coffee 10 | # 11 | # from the command line. You should do both kinds of testing before pushing. 12 | # 13 | # 14 | # These tests are pretty simple and primitive. The reality is, google's browserchannel 15 | # client library is pretty bloody well tested. (I'm not interested in rewriting that test suite) 16 | # 17 | # However, its important to do some sanity checks on the exported browserchannel bits to 18 | # make sure closure isn't doing anything wacky. Also this acts as a nice little integration 19 | # test for the server, _and_ its useful to make sure that all the browsers node-browserchannel 20 | # supports are behaving themselves. 21 | # 22 | # Oh yeah, and these tests will be run on the nodejs version of browserchannel, which has 23 | # a lot of silly little parts. 24 | # 25 | # These tests will also be useful if the browserchannel protocol ever changes. 26 | # 27 | # Interestingly, most of the benefits of this test suite come from a single test (really, any 28 | # test). If any test passes, they'll all probably pass. 29 | # 30 | # 31 | # ## Known Issues 32 | # 33 | # There's three weird issues with this test suite: 34 | # 35 | # - Sometimes (maybe, 1 in 10) times the test is run from nodejs, it dies in a weird inconsistant 36 | # state. 37 | # - After a test run, 4 sessions are allowed to time out by the server. (Its odd because I'm calling 38 | # disconnect() in tearDown). 39 | # 40 | 41 | assert = require 'assert' 42 | 43 | if typeof window is 'undefined' 44 | try 45 | require('./runserver').listen 4321 46 | catch e 47 | console.warn e.stack 48 | 49 | bc = require '..' 50 | # If coffeescript declares a variable called 'BCSocket' here, it will shadow 51 | # the BCSocket variable that is already defined in the browser. Doing it this 52 | # way is pretty ugly, but it works and the ugliness is constrained to a test. 53 | global.BCSocket = bc.BCSocket 54 | bc.setDefaultLocation 'http://localhost:4321' 55 | 56 | # This is a little logging function for old IE. It adds a log() function which 57 | # simply appends HTML messages to the document. 58 | window?.log = log = (str) -> 59 | div = document.createElement 'div' 60 | div.innerHTML = str 61 | document.body.appendChild div 62 | 63 | suite 'bcsocket', -> 64 | # IE6 takes about 12 seconds to do the large stress test 65 | @timeout 20000 66 | 67 | teardown (callback) -> 68 | if @socket? and @socket.readyState isnt BCSocket.CLOSED 69 | @socket.onclose = -> callback() 70 | @socket.close() 71 | @socket = null 72 | else 73 | callback() 74 | 75 | # These match websocket codes 76 | test 'states and errors are mapped', -> 77 | assert.strictEqual BCSocket.CONNECTING, 0 78 | assert.strictEqual BCSocket.OPEN, 1 79 | assert.strictEqual BCSocket.CLOSING, 2 80 | assert.strictEqual BCSocket.CLOSED, 3 81 | 82 | assert.strictEqual BCSocket.prototype.CONNECTING, 0 83 | assert.strictEqual BCSocket.prototype.OPEN, 1 84 | assert.strictEqual BCSocket.prototype.CLOSING, 2 85 | assert.strictEqual BCSocket.prototype.CLOSED, 3 86 | 87 | assert.strictEqual BCSocket.canSendWhileConnecting, true 88 | assert.strictEqual BCSocket.prototype.canSendWhileConnecting, true 89 | 90 | assert.strictEqual BCSocket.canSendJSON, true 91 | assert.strictEqual BCSocket.prototype.canSendJSON, true 92 | 93 | # Can we connect to the server? 94 | test 'connect', (done) -> 95 | @socket = new BCSocket '/notify' 96 | assert.strictEqual @socket.readyState, BCSocket.CONNECTING 97 | 98 | openCalled = false 99 | 100 | @socket.onopen = => 101 | assert.strictEqual @socket.readyState, BCSocket.OPEN 102 | openCalled = true 103 | 104 | @socket.onerror = (reason) -> 105 | throw new Error reason 106 | 107 | @socket.onmessage = (message) -> 108 | assert.deepEqual message.data, {appVersion: null} 109 | assert.ok openCalled 110 | done() 111 | 112 | # The socket interface exposes browserchannel's app version thingy through 113 | # option arguments 114 | test 'connect sends app version', (done) -> 115 | @socket = new BCSocket '/notify', appVersion: 321 116 | 117 | @socket.onmessage = (message) -> 118 | assert.deepEqual message.data, {appVersion:321} 119 | done() 120 | 121 | # BrowserChannel's native send method sends a string->string map. 122 | # 123 | # I want to test that I can send and recieve messages both before we've connected 124 | # (they should be sent as soon as the connection is established) and after the 125 | # connection has opened normally. 126 | suite 'send maps', -> 127 | # I'll throw some random unicode characters in here just to make sure... 128 | data = {'foo': 'bar', 'zot': '(◔ ◡ ◔)'} 129 | 130 | m = (callback) -> (done) -> 131 | @socket = new BCSocket '/echomap', appVersion: 321 132 | @socket.onmessage = (message) -> 133 | assert.deepEqual message.data, data 134 | done() 135 | 136 | callback.apply this 137 | 138 | test 'immediately', m -> 139 | @socket.sendMap data 140 | 141 | test 'after we have connected', m -> 142 | @socket.onopen = => 143 | @socket.sendMap data 144 | 145 | # I'll also test the normal send method. This is pretty much the same as above, whereby 146 | # I'll do the test two ways. 147 | suite 'can send and receive', -> 148 | test 'unicode', (done) -> 149 | # Vim gets formatting errors with the cat face glyph here. Sad. 150 | data = '⚗☗⚑☯' 151 | 152 | @socket = new BCSocket '/echo', appVersion: 321 153 | @socket.onmessage = (message) -> 154 | assert.deepEqual message.data, data 155 | done() 156 | 157 | @socket.onopen = => 158 | @socket.send data 159 | 160 | 161 | 162 | suite 'JSON messages', -> 163 | # Vim gets formatting errors with the cat face glyph here. Sad. 164 | data = [null, 1.5, "hi", {}, [1,2,3], '⚗☗⚑☯'] 165 | 166 | m = (callback) -> (done) -> 167 | # Using the /echo server not /echomap 168 | @socket = new BCSocket '/echo', appVersion: 321 169 | @socket.onmessage = (message) -> 170 | assert.deepEqual message.data, data 171 | done() 172 | 173 | callback.apply this 174 | 175 | test 'immediately', m -> 176 | # Calling send() instead of sendMap() 177 | @socket.send data 178 | 179 | test 'after we have connected', m -> 180 | @socket.onopen = => 181 | @socket.send data 182 | 183 | suite 'string messages', -> 184 | # Vim gets formatting errors with the cat face glyph here. Sad. 185 | data = ["hi", "", " ", "\n", "\t", '⚗☗⚑☯', "\u2028 \u2029", ('x' for [1..1000]).join()] 186 | 187 | # I'm going to send each message in the array in sequence. We should get 188 | # them back in the same sequence. 189 | pos = 0 190 | 191 | m = (callback) -> (done) -> 192 | # Using the /echo server not /echomap 193 | @socket = new BCSocket '/echo', appVersion: 321 194 | @socket.onmessage = (message) -> 195 | assert pos < data.length 196 | assert.deepEqual message.data, data[pos++] 197 | done() if pos == data.length 198 | 199 | callback.apply this 200 | 201 | test 'immediately', m -> 202 | pos = 0 203 | # Calling send() instead of sendMap() 204 | @socket.send str for str in data 205 | return 206 | 207 | test 'after we have connected', m -> 208 | pos = 0 209 | @socket.onopen = => 210 | @socket.send str for str in data 211 | return 212 | 213 | # This is a little stress test to make sure I haven't missed anything. 214 | # Sending and recieving this much data pushes the client to use multiple 215 | # forward channel connections. It doesn't use multiple backchannel 216 | # connections - I should probably put some logic there whereby I close the 217 | # backchannel after awhile. 218 | test 'Lots of data', (done) -> 219 | num = 5000 220 | 221 | @socket = new BCSocket '/echomap' 222 | 223 | received = 0 224 | @socket.onmessage = (message) -> 225 | assert.equal message.data.data, received 226 | received++ 227 | 228 | done() if received == num 229 | 230 | setTimeout => 231 | # Maps aren't actual JSON. They're just key-value pairs. I don't need to 232 | # encode i as a string here, but thats now its sent anyway. 233 | @socket.sendMap {data:"#{i}", juuuuuuuuuuuuuuuuunnnnnnnnnk:'waaaazzzzzzuuuuuppppppp'} for i in [0...num] 234 | , 0 235 | 236 | 237 | # I have 2 disconnect servers which have slightly different timing regarding 238 | # when they call close() on the session. If close is called immediately, the 239 | # initial bind request is rejected with a 403 response, before the client 240 | # connects. 241 | test 'disconnecting immediately results in REQUEST_FAILED and a 403', (done) -> 242 | @socket = new BCSocket '/dc1', reconnect: no 243 | 244 | @socket.onopen = -> throw new Error 'Socket should not have opened' 245 | 246 | onErrorCalled = no 247 | @socket.onerror = (message, errCode) => 248 | assert.strictEqual message, 'Request failed' 249 | assert.strictEqual errCode, 2 250 | onErrorCalled = yes 251 | 252 | @socket.onclose = -> 253 | # This will be called because technically, the websocket does go into the 254 | # close state! 255 | # 256 | # This is exactly what websockets do. 257 | assert.ok onErrorCalled 258 | 259 | done() 260 | 261 | test 'disconnecting momentarily allows the client to connect, then onclose() is called', (done) -> 262 | @socket = new BCSocket '/dc2', failFast: yes 263 | 264 | onErrorCalled = no 265 | @socket.onerror = (message, errCode) => 266 | # The error code varies here, depending on some timing parameters & 267 | # browser. I've seen NO_DATA, REQUEST_FAILED and UNKNOWN_SESSION_ID. 268 | assert.strictEqual @socket.readyState, @socket.CLOSING 269 | assert.ok message 270 | assert.ok errCode 271 | onErrorCalled = yes 272 | 273 | @socket.onclose = (reason, pendingMaps, undeliveredMaps) => 274 | # The error code varies here, depending on some timing parameters & browser. 275 | # These will probably be undefined, but == will catch that. 276 | assert.strictEqual @socket.readyState, @socket.CLOSED 277 | assert.equal pendingMaps, null 278 | assert.equal undeliveredMaps, null 279 | assert.ok onErrorCalled 280 | done() 281 | 282 | test 'passing a previous session will ghost that session', (done) -> 283 | @socket1 = new BCSocket '/echo' 284 | @socket1.onopen = => 285 | @socket2 = new BCSocket '/echo', prev:@socket1 286 | 287 | @socket1.onclose = => 288 | @socket2.close() 289 | done() 290 | 291 | suite 'The client keeps reconnecting', -> 292 | m = (base) -> (done) -> 293 | @socket = new BCSocket base, failFast: yes, reconnect: yes, reconnectTime: 300 294 | 295 | openCount = 0 296 | 297 | @socket.onopen = => 298 | throw new Error 'Should not keep trying to open once the test is done' if openCount == 2 299 | 300 | assert.strictEqual @socket.readyState, @socket.OPEN 301 | 302 | @socket.onclose = (reason, pendingMaps, undeliveredMaps) => 303 | assert.strictEqual @socket.readyState, @socket.CLOSED 304 | 305 | assert openCount < 2 306 | openCount++ 307 | if openCount is 2 308 | # Tell the socket to stop trying to connect 309 | @socket.close() 310 | done() 311 | 312 | test 'When the connection fails', m('dc1') 313 | # 'When the connection dies': m('dc3') 314 | 315 | suite 'stop', -> 316 | makeTest = (base) -> (done) -> 317 | # We don't need failFast for stop. 318 | @socket = new BCSocket base 319 | 320 | onErrorCalled = no 321 | @socket.onerror = (message, errCode) => 322 | assert.strictEqual @socket.readyState, @socket.CLOSING 323 | assert.strictEqual message, 'Stopped by server' 324 | assert.strictEqual errCode, 7 325 | onErrorCalled = yes 326 | 327 | @socket.onclose = (reason, pendingMaps, undeliveredMaps) => 328 | # These will probably be undefined, but == will catch that. 329 | assert.strictEqual @socket.readyState, @socket.CLOSED 330 | assert.equal pendingMaps, null 331 | assert.equal undeliveredMaps, null 332 | assert.strictEqual reason, 'Stopped by server' 333 | assert.ok onErrorCalled 334 | done() 335 | 336 | test 'on connect', makeTest 'stop1' 337 | test 'after connect', makeTest 'stop2' 338 | 339 | # We need to be able to send \u2028 and \u2029 340 | # http://timelessrepo.com/json-isnt-a-javascript-subset 341 | test 'Line separator and paragraph separators work', (done) -> 342 | @socket = new BCSocket '/utfsep', appVersion: 321 343 | 344 | @socket.onmessage = (message) -> 345 | assert.strictEqual message.data, "\u2028 \u2029" 346 | done() 347 | 348 | # We should be able to specify GET variables to be sent with every request. 349 | test 'extraParams are passed to the server', (done) -> 350 | @socket = new BCSocket '/extraParams', extraParams: foo: 'bar' 351 | 352 | @socket.onmessage = (message) -> 353 | assert.strictEqual message.data.foo, 'bar' 354 | done() 355 | 356 | test 'Session affinity tokens are generated by default', (done) -> 357 | @socket = new BCSocket '/extraParams' 358 | affinity = @socket.affinity 359 | 360 | @socket.onmessage = (message) -> 361 | assert.strictEqual message.data.a, affinity 362 | done() 363 | 364 | test 'Session affinity tokens can be set manually', (done) -> 365 | @socket = new BCSocket '/extraParams', affinity: 'custom token' 366 | 367 | @socket.onmessage = (message) -> 368 | assert.strictEqual message.data.a, 'custom token' 369 | done() 370 | 371 | test 'Session affinity GET variable can be modified', (done) -> 372 | @socket = new BCSocket '/extraParams', affinityParam: 'avoidConflict' 373 | affinity = @socket.affinity 374 | 375 | @socket.onmessage = (message) -> 376 | assert.strictEqual message.data.avoidConflict, affinity 377 | done() 378 | 379 | test 'Session affinity tokens can be disabled', (done) -> 380 | @socket = new BCSocket '/extraParams', affinity: null 381 | 382 | @socket.onmessage = (message) -> 383 | assert.strictEqual message.data.a, undefined 384 | done() 385 | 386 | test 'Extra headers are sent to the server', (done) -> 387 | @socket = new BCSocket '/extraHeaders', extraHeaders: {'X-Style': 'Fabulous'} 388 | 389 | @socket.onmessage = (message) -> 390 | assert.strictEqual message.data['x-style'], 'Fabulous' 391 | done() 392 | 393 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script/register 2 | --reporter spec 3 | --ui tdd 4 | --check-leaks 5 | -------------------------------------------------------------------------------- /test/runserver.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | # This file hosts a web server which exposes a bunch of browserchannel clients 3 | # which respond in different ways to requests. 4 | 5 | fs = require 'fs' 6 | express = require 'express' 7 | browserChannel = require('..').server 8 | coffee = require 'coffee-script' 9 | 10 | app = express() 11 | 12 | app.use express.static "#{__dirname}/web" 13 | app.use express.static "#{__dirname}/../node_modules/mocha" # for mocha.js, mocha.css 14 | #app.use express.logger 'dev' 15 | 16 | # Compile and host the tests. 17 | app.use (req, res, next) -> 18 | return next() unless req.url is '/tests.js' 19 | f = fs.readFileSync require.resolve('./bcsocket'), 'utf8' 20 | f = "require = (m) -> window[m]\n" + f 21 | res.setHeader 'Content-Type', 'application/javascript' 22 | res.end coffee.compile f 23 | 24 | # So, I really want to remove the 'base:' property from browserchannel and just 25 | # use express's router's mechanism. It works fine, except that options.base defaults 26 | # to '/channel', so you have to override it. 27 | # 28 | # I missed a great opportunity to make this change in browserchannel 2.0. 29 | 30 | # When a client connects, send it a simple message saying its app version 31 | app.use '/notify', browserChannel base:'', (session) -> 32 | session.send {appVersion: session.appVersion} 33 | 34 | # Echo back any JSON messages a client sends. 35 | app.use browserChannel base:'/echo', (session) -> 36 | session.on 'message', (message) -> 37 | session.send message 38 | 39 | # Echo back any maps the client sends 40 | app.use browserChannel base:'/echomap', (session) -> 41 | session.on 'map', (message) -> 42 | session.send message 43 | 44 | # This server aborts incoming sessions *immediately*. 45 | app.use browserChannel base:'/dc1', (session) -> 46 | session.close() 47 | 48 | # This server aborts incoming sessions after sending 49 | app.use browserChannel base:'/dc2', (session) -> 50 | process.nextTick -> 51 | session.close() 52 | 53 | app.use browserChannel base:'/dc3', (session) -> 54 | setTimeout (-> session.close()), 100 55 | 56 | # Send a stop() message immediately 57 | app.use browserChannel base:'/stop1', (session) -> 58 | session.stop() 59 | 60 | # Send a stop() message in a moment 61 | app.use browserChannel base:'/stop2', (session) -> 62 | process.nextTick -> 63 | session.stop() 64 | 65 | # Send the characters that aren't valid javascript as literals 66 | # http://timelessrepo.com/json-isnt-a-javascript-subset 67 | app.use browserChannel base:'/utfsep', (session) -> 68 | session.send "\u2028 \u2029" 69 | #session.send {"\u2028 \u2029"} 70 | 71 | app.use browserChannel base:'/extraParams', (session) -> 72 | session.send session.query 73 | 74 | app.use browserChannel base:'/extraHeaders', (session) -> 75 | session.send session.headers 76 | 77 | server = module.exports = require('http').createServer(app) 78 | 79 | if require.main == module 80 | server.listen 4321 81 | console.log 'Point your browser at http://localhost:4321/' 82 | -------------------------------------------------------------------------------- /test/web/assert.js: -------------------------------------------------------------------------------- 1 | // http://wiki.commonjs.org/wiki/Unit_Testing/1.0 2 | // 3 | // THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8! 4 | // 5 | // Copyright (c) 2011 Jxck 6 | // 7 | // Originally from node.js (http://nodejs.org) 8 | // Copyright Joyent, Inc. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the 'Software'), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | (function(global) { 28 | 29 | // Object.create compatible in IE 30 | var create = Object.create || function(p) { 31 | if (!p) throw Error('no type'); 32 | function f() {}; 33 | f.prototype = p; 34 | return new f(); 35 | }; 36 | 37 | // UTILITY 38 | var util = { 39 | inherits: function(ctor, superCtor) { 40 | ctor.super_ = superCtor; 41 | ctor.prototype = create(superCtor.prototype, { 42 | constructor: { 43 | value: ctor, 44 | enumerable: false, 45 | writable: true, 46 | configurable: true 47 | } 48 | }); 49 | } 50 | }; 51 | 52 | var pSlice = Array.prototype.slice; 53 | 54 | // from https://github.com/substack/node-deep-equal 55 | var Object_keys = typeof Object.keys === 'function' 56 | ? Object.keys 57 | : function (obj) { 58 | var keys = []; 59 | for (var key in obj) keys.push(key); 60 | return keys; 61 | } 62 | ; 63 | 64 | // 1. The assert module provides functions that throw 65 | // AssertionError's when particular conditions are not met. The 66 | // assert module must conform to the following interface. 67 | 68 | var assert = ok; 69 | 70 | global['assert'] = assert; 71 | 72 | if (typeof module === 'object' && typeof module.exports === 'object') { 73 | module.exports = assert; 74 | }; 75 | 76 | // 2. The AssertionError is defined in assert. 77 | // new assert.AssertionError({ message: message, 78 | // actual: actual, 79 | // expected: expected }) 80 | 81 | assert.AssertionError = function AssertionError(options) { 82 | this.name = 'AssertionError'; 83 | this.message = options.message; 84 | this.actual = options.actual; 85 | this.expected = options.expected; 86 | this.operator = options.operator; 87 | var stackStartFunction = options.stackStartFunction || fail; 88 | 89 | if (Error.captureStackTrace) { 90 | Error.captureStackTrace(this, stackStartFunction); 91 | } 92 | }; 93 | 94 | // assert.AssertionError instanceof Error 95 | util.inherits(assert.AssertionError, Error); 96 | 97 | function replacer(key, value) { 98 | if (value === undefined) { 99 | return '' + value; 100 | } 101 | if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) { 102 | return value.toString(); 103 | } 104 | if (typeof value === 'function' || value instanceof RegExp) { 105 | return value.toString(); 106 | } 107 | return value; 108 | } 109 | 110 | function truncate(s, n) { 111 | if (typeof s == 'string') { 112 | return s.length < n ? s : s.slice(0, n); 113 | } else { 114 | return s; 115 | } 116 | } 117 | 118 | assert.AssertionError.prototype.toString = function() { 119 | if (this.message) { 120 | return [this.name + ':', this.message].join(' '); 121 | } else { 122 | return [ 123 | this.name + ':', 124 | truncate(JSON.stringify(this.actual, replacer), 128), 125 | this.operator, 126 | truncate(JSON.stringify(this.expected, replacer), 128) 127 | ].join(' '); 128 | } 129 | }; 130 | 131 | // At present only the three keys mentioned above are used and 132 | // understood by the spec. Implementations or sub modules can pass 133 | // other keys to the AssertionError's constructor - they will be 134 | // ignored. 135 | 136 | // 3. All of the following functions must throw an AssertionError 137 | // when a corresponding condition is not met, with a message that 138 | // may be undefined if not provided. All assertion methods provide 139 | // both the actual and expected values to the assertion error for 140 | // display purposes. 141 | 142 | function fail(actual, expected, message, operator, stackStartFunction) { 143 | throw new assert.AssertionError({ 144 | message: message, 145 | actual: actual, 146 | expected: expected, 147 | operator: operator, 148 | stackStartFunction: stackStartFunction 149 | }); 150 | } 151 | 152 | // EXTENSION! allows for well behaved errors defined elsewhere. 153 | assert.fail = fail; 154 | 155 | // 4. Pure assertion tests whether a value is truthy, as determined 156 | // by !!guard. 157 | // assert.ok(guard, message_opt); 158 | // This statement is equivalent to assert.equal(true, !!guard, 159 | // message_opt);. To test strictly for the value true, use 160 | // assert.strictEqual(true, guard, message_opt);. 161 | 162 | function ok(value, message) { 163 | if (!!!value) fail(value, true, message, '==', assert.ok); 164 | } 165 | assert.ok = ok; 166 | 167 | // 5. The equality assertion tests shallow, coercive equality with 168 | // ==. 169 | // assert.equal(actual, expected, message_opt); 170 | 171 | assert.equal = function equal(actual, expected, message) { 172 | if (actual != expected) fail(actual, expected, message, '==', assert.equal); 173 | }; 174 | 175 | // 6. The non-equality assertion tests for whether two objects are not equal 176 | // with != assert.notEqual(actual, expected, message_opt); 177 | 178 | assert.notEqual = function notEqual(actual, expected, message) { 179 | if (actual == expected) { 180 | fail(actual, expected, message, '!=', assert.notEqual); 181 | } 182 | }; 183 | 184 | // 7. The equivalence assertion tests a deep equality relation. 185 | // assert.deepEqual(actual, expected, message_opt); 186 | 187 | assert.deepEqual = function deepEqual(actual, expected, message) { 188 | if (!_deepEqual(actual, expected)) { 189 | fail(actual, expected, message, 'deepEqual', assert.deepEqual); 190 | } 191 | }; 192 | 193 | function _deepEqual(actual, expected) { 194 | // 7.1. All identical values are equivalent, as determined by ===. 195 | if (actual === expected) { 196 | return true; 197 | 198 | // } else if (Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { 199 | // if (actual.length != expected.length) return false; 200 | // 201 | // for (var i = 0; i < actual.length; i++) { 202 | // if (actual[i] !== expected[i]) return false; 203 | // } 204 | // 205 | // return true; 206 | // 207 | // 7.2. If the expected value is a Date object, the actual value is 208 | // equivalent if it is also a Date object that refers to the same time. 209 | } else if (actual instanceof Date && expected instanceof Date) { 210 | return actual.getTime() === expected.getTime(); 211 | 212 | // 7.3 If the expected value is a RegExp object, the actual value is 213 | // equivalent if it is also a RegExp object with the same source and 214 | // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). 215 | } else if (actual instanceof RegExp && expected instanceof RegExp) { 216 | return actual.source === expected.source && 217 | actual.global === expected.global && 218 | actual.multiline === expected.multiline && 219 | actual.lastIndex === expected.lastIndex && 220 | actual.ignoreCase === expected.ignoreCase; 221 | 222 | // 7.4. Other pairs that do not both pass typeof value == 'object', 223 | // equivalence is determined by ==. 224 | } else if (typeof actual != 'object' && typeof expected != 'object') { 225 | return actual == expected; 226 | 227 | // 7.5 For all other Object pairs, including Array objects, equivalence is 228 | // determined by having the same number of owned properties (as verified 229 | // with Object.prototype.hasOwnProperty.call), the same set of keys 230 | // (although not necessarily the same order), equivalent values for every 231 | // corresponding key, and an identical 'prototype' property. Note: this 232 | // accounts for both named and indexed properties on Arrays. 233 | } else { 234 | return objEquiv(actual, expected); 235 | } 236 | } 237 | 238 | function isUndefinedOrNull(value) { 239 | return value === null || value === undefined; 240 | } 241 | 242 | function isArguments(object) { 243 | return Object.prototype.toString.call(object) == '[object Arguments]'; 244 | } 245 | 246 | function objEquiv(a, b) { 247 | if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) 248 | return false; 249 | // an identical 'prototype' property. 250 | if (a.prototype !== b.prototype) return false; 251 | //~~~I've managed to break Object.keys through screwy arguments passing. 252 | // Converting to array solves the problem. 253 | if (isArguments(a)) { 254 | if (!isArguments(b)) { 255 | return false; 256 | } 257 | a = pSlice.call(a); 258 | b = pSlice.call(b); 259 | return _deepEqual(a, b); 260 | } 261 | try { 262 | var ka = Object_keys(a), 263 | kb = Object_keys(b), 264 | key, i; 265 | } catch (e) {//happens when one is a string literal and the other isn't 266 | return false; 267 | } 268 | // having the same number of owned properties (keys incorporates 269 | // hasOwnProperty) 270 | if (ka.length != kb.length) 271 | return false; 272 | //the same set of keys (although not necessarily the same order), 273 | ka.sort(); 274 | kb.sort(); 275 | //~~~cheap key test 276 | for (i = ka.length - 1; i >= 0; i--) { 277 | if (ka[i] != kb[i]) 278 | return false; 279 | } 280 | //equivalent values for every corresponding key, and 281 | //~~~possibly expensive deep test 282 | for (i = ka.length - 1; i >= 0; i--) { 283 | key = ka[i]; 284 | if (!_deepEqual(a[key], b[key])) return false; 285 | } 286 | return true; 287 | } 288 | 289 | // 8. The non-equivalence assertion tests for any deep inequality. 290 | // assert.notDeepEqual(actual, expected, message_opt); 291 | 292 | assert.notDeepEqual = function notDeepEqual(actual, expected, message) { 293 | if (_deepEqual(actual, expected)) { 294 | fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); 295 | } 296 | }; 297 | 298 | // 9. The strict equality assertion tests strict equality, as determined by ===. 299 | // assert.strictEqual(actual, expected, message_opt); 300 | 301 | assert.strictEqual = function strictEqual(actual, expected, message) { 302 | if (actual !== expected) { 303 | fail(actual, expected, message, '===', assert.strictEqual); 304 | } 305 | }; 306 | 307 | // 10. The strict non-equality assertion tests for strict inequality, as 308 | // determined by !==. assert.notStrictEqual(actual, expected, message_opt); 309 | 310 | assert.notStrictEqual = function notStrictEqual(actual, expected, message) { 311 | if (actual === expected) { 312 | fail(actual, expected, message, '!==', assert.notStrictEqual); 313 | } 314 | }; 315 | 316 | function expectedException(actual, expected) { 317 | if (!actual || !expected) { 318 | return false; 319 | } 320 | 321 | if (Object.prototype.toString.call(expected) == '[object RegExp]') { 322 | return expected.test(actual); 323 | } else if (actual instanceof expected) { 324 | return true; 325 | } else if (expected.call({}, actual) === true) { 326 | return true; 327 | } 328 | 329 | return false; 330 | } 331 | 332 | function _throws(shouldThrow, block, expected, message) { 333 | var actual; 334 | 335 | if (typeof expected === 'string') { 336 | message = expected; 337 | expected = null; 338 | } 339 | 340 | try { 341 | block(); 342 | } catch (e) { 343 | actual = e; 344 | } 345 | 346 | message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + 347 | (message ? ' ' + message : '.'); 348 | 349 | if (shouldThrow && !actual) { 350 | fail(actual, expected, 'Missing expected exception' + message); 351 | } 352 | 353 | if (!shouldThrow && expectedException(actual, expected)) { 354 | fail(actual, expected, 'Got unwanted exception' + message); 355 | } 356 | 357 | if ((shouldThrow && actual && expected && 358 | !expectedException(actual, expected)) || (!shouldThrow && actual)) { 359 | throw actual; 360 | } 361 | } 362 | 363 | // 11. Expected to throw an error: 364 | // assert.throws(block, Error_opt, message_opt); 365 | 366 | assert.throws = function(block, /*optional*/error, /*optional*/message) { 367 | _throws.apply(this, [true].concat(pSlice.call(arguments))); 368 | }; 369 | 370 | // EXTENSION! This is annoying to write outside this module. 371 | assert.doesNotThrow = function(block, /*optional*/message) { 372 | _throws.apply(this, [false].concat(pSlice.call(arguments))); 373 | }; 374 | 375 | assert.ifError = function(err) { if (err) {throw err;}}; 376 | 377 | if (typeof define === 'function' && define.amd) { 378 | define('assert', function () { 379 | return assert; 380 | }); 381 | } 382 | 383 | })(this); 384 | 385 | -------------------------------------------------------------------------------- /test/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | BrowserChannel Test Suite 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 17 | 18 | 19 | --------------------------------------------------------------------------------