├── .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 | [](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("
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------