diff --git a/History.md b/History.md index 8eb0a6fc0b..e6186eda6b 100644 --- a/History.md +++ b/History.md @@ -1,127 +1,151 @@ -0.6.0 / 2010-11-01 +0.6.3 / 2010-12-23 ================== -* Make sure to only write to open transports (thanks JohnDav) -* _open is still false, so destroy the connection immediately upon websocket error -* Make sure to disconnect directly onClose if the client is not handshaked and he can't possibly reconnect -* Make sure to end and destroy connection onDisconnect (for timeouts) -* Added missing .listen() call to example. Fixes #80. Thanks @machee -* Invalid transport test completed -* Initial stab at trying to detect invalid transport responses -* Make sure to provide a default for `log` if no log key was provided (internal) -* Removed unnecessary file extension verification when serving the client -* Removed unnecessary Client check upon connection -* Added support for /socket.io/WebSocketMain.swf -* Added test for /socket.io/WebSocketMain.swf -* Client serving ETag testing -* Added htmlfile transport tests -* Added extra byte to IE iframe bytes padding -* Invalid session id test -* end() before destroy()ing the socket for non-WebSocket or non-valid Upgrade requests -* Added test for non-socket.io requests -* Simplified index.js tests -* Moved listener tests into listener.js -* Make sure to call .end() when listening on connection 'end' event -* Make sure the file descriptor is destroyed on disconnection -* Fix for websocket client tracking test -* Inline (same port) flash socket policy request. -* If the server is not run with root privileges, then the flashsocket -transport will instead listen to all new connections on the main port -for policy requests. Flash policy requests happen to both port 843 and -the destination port: -http://www.lightsphere.com/dev/articles/flash_socket_policy.html - -* [websocket test] Fix sending message to client upon connecting -* [websocket test] Fix for connection and handshake test -* [client files serving] Leverage end() write() call -* [client serving] Make sure to not do a useless file lookup when file is cached -* Finished json encoding test -* Look for the heartbeat in the decoded message -* Refactored websocket transports tests to match polling/multipart helpers -* Added coverage testing to Makefile -* Added heartbeat test to multipart -* Added buffered messages test for multipart -* Added assertions for `connected` property for all the tests -* Multipart clients tracking test -* Multipart client>server message sending test -* Make sure to only close the client stream when the roundtrip is complete -* Multipart connection and handshake tests: - - Implemented HTTP client on top of net.Stream with multipart boundary parsing for testing - - Test for connection / server>client message sending -* Removed unnecessary check for this.connection (since we now access the socket through req.connection for all transports) -* Test for `duration` parameter -* Added `make example` to Makefile -* Added clients tracking test for long polling -* Added message buffering test for long polling -* Improve this.request/this.response/this.connection -* Add 'end' listener onConnect, applies to all transports -* Improved error handling onConnect -* Remove legacy `flush` calls -* Removed unnecessary closeTimeout clearing in jsonp polling -* Make sure to close on disconnect if _open = true -* Clear disconnection timeout on disconnection (double check) -* Make sure to clear closeTimeout for polling transports on close. -* Replaced empty with null in log option -* Comma first style for client serving tests -* Long polling integration tests -* Test for heartbeat message -* Added heartbeat timeout test -* Support for listener#log false -* Corrected onConnect signature to support a request and a socket, or a request and a response. -* Removed error checking for non-upgradeable sockets, since they'll be destroyed, and error handling is done onConnect -* Added tests for websocket client tracking -* Added tests for websocket message buffering -* Make sure disconnect timeout is cleared on websocket re-connect -* Updated the flash socket with error detection, and readystate detection. -* This is needed because when a error occures we close down the connection, -* and the stream will become unwriteable. -* Also changed to a single write instead of multiple writes. -* Moved error handling to onConnect to avoid messing with the http.Server global error handlers -* Do special error handling for websocket -* Clearing heartbeat interval upon closing the connection -* Added error listeners, if theses errors are not correcly caught, they will leak memory. -* This caused http://speedo.no.de/ to go up from 1mb per connection after a ECONNECTRESET message -* Added encode=UTF-8 in jsonp-polling.js and xhr-polling.js since UTF-8 is the default encoding for http.ServerResponse.write -* Replaced string.length with Buffer.byteLength in jsonp-polling.js, listener.js and xhr-polling.js because content-length header requires number of bytes and not the number of symbols in string -* Fix COR headers/requests for different ports on Safari. -* Clearing the references to request, response and connection upon disconnect. -* Every require is blocking and requiring the sys module over and over and over again just makes no sense + it hurt performance.. Not to mention.. that it's already included. -* Socket.IO-node now serves the client out of the box for easier implementation -* Memory caching and ETag support for static files -* Tests -* Simplified demo even further thanks to new static file serving -* Failing to pass an origin header would throw an exception and crash the server. Added some handling. -* .connected renamed to ._open, and adopted proper `connected` (fixes #41) -* example/client updated to latest socket.io client -* Better checking of WebSocket connections -* Better handling of SSL location (thanks @jdub) -* Fix for cross-domain websocket (fixes #42) -* Removed clients/clientsIndex and only using the index (fixes #28) -* Fixed WebSocket location header for ws/wss (Thanks @jdub, Fixes #40) -* Cross domain issues with xhr-polling addressed. Thanks Niko Kaiser (@nicokaiser) -* Added origin verification for incoming data. -* Make sure pathname is set (thanks steadicat & swarmation team) -* Fix for accessing routes that being with the namespace but are not a connection attempt. Thanks @steadicat from swarmation -* JSONP-polling support -* Graceful closing of connection for invalid websocket clients -* Make it possible to just require 'socket.io' -* Make sure to abort the connect() method upon bad upgrade / origin verification -* Support for automatic JSON encoding/decoding -* Simplified chat example to take advantage of JSON encoding/decoding -* Removed fs sync call from example -* Better `how to use` -* Make sure to send content-type text/plain to `ok` POST responses + * Changed polling default duration to 50 seconds + * might > will Adjusted to 85 column limit + * Support for resources that include slashes. Thanks @schamane + * Lazy loading of transports. Thanks @technoweenie Fixed README transports list + * OpenSSL clarifications (thanks @bmnds) + * Support for HAProxy load balancing (thanks Brian McKelvey) Backported Parser from 0.7 + * Fixed HTTP API in example (was outdated). Thanks deedubs + * 0.3 compatibility (thanks Arnout) + * `client.broadcast` now 300% faster Cleaned up chat example + * fixed bad pluralization. + * cleaned up grammar, missing punctuation, etc. + * Restored global `netserver` for flashsocket Now supporting `flashPolicyServer` option (thanks Arnout) Tests passing with and without sudo/root user Fixed noDelay/timeout/utf-8 for draft 76 (accidental typo) + * Close the netServer when the main http server closes, this way the event loop does not keep running. NOTE: this is patch for node 0.2.X, this is not required for node 0.3.X + * Fallback to try{}catch handling for node < 0.2.4 , node 0.3.X seems to capture the errors correctly using the error event. + * Added the flash policy server, it's enabled by default but can be turned off if needed. Socket.io will automatically fallback to serving the policy file inline if server is disabled or unable to start up. + * Make sure to only write to open transports (thanks JohnDav) + * _open is still false, so destroy the connection immediately upon websocket error + * Make sure .connection is not null on 'end' + * Proper fix for invalid websocket key 0.6.1 / 2010-11-08 -* Restored flash policy server, but with these changes: - - It's contingent on the listener flashPolicyServer option - - It's started by default if socket.io is started with root access - - It correctly closes the netserver upon all the dependent http servers being closed - - The handler for the inline request is still there regardless. This is important in the following circumstances, and has no performance hit - - The port 843 is filtered - - Flash at some point enables us to skip 843 checking altogether - - Tests compatibility -* Fixed connection timeout, noDelay and socket encoding for draft 76 (had been accidentally moved into the `else` block) -* Some stylistic fixes \ No newline at end of file + * Restored flash policy server, but with these changes: + - It's contingent on the listener flashPolicyServer option + - It's started by default if socket.io is started with root access + - It correctly closes the netserver upon all the dependent http servers being closed + - The handler for the inline request is still there regardless. This is important in the following circumstances, and has no performance hit + - The port 843 is filtered + - Flash at some point enables us to skip 843 checking altogether + - Tests compatibility + * Fixed connection timeout, noDelay and socket encoding for draft 76 (had been accidentally moved into the `else` block) + * Some stylistic fixes + +0.6.0 / 2010-11-01 +================== + + * Make sure to only write to open transports (thanks JohnDav) + * _open is still false, so destroy the connection immediately upon websocket error + * Make sure to disconnect directly onClose if the client is not handshaked and he can't possibly reconnect + * Make sure to end and destroy connection onDisconnect (for timeouts) + * Added missing .listen() call to example. Fixes #80. Thanks @machee + * Invalid transport test completed + * Initial stab at trying to detect invalid transport responses + * Make sure to provide a default for `log` if no log key was provided (internal) + * Removed unnecessary file extension verification when serving the client + * Removed unnecessary Client check upon connection + * Added support for /socket.io/WebSocketMain.swf + * Added test for /socket.io/WebSocketMain.swf + * Client serving ETag testing + * Added htmlfile transport tests + * Added extra byte to IE iframe bytes padding + * Invalid session id test + * end() before destroy()ing the socket for non-WebSocket or non-valid Upgrade requests + * Added test for non-socket.io requests + * Simplified index.js tests + * Moved listener tests into listener.js + * Make sure to call .end() when listening on connection 'end' event + * Make sure the file descriptor is destroyed on disconnection + * Fix for websocket client tracking test + * Inline (same port) flash socket policy request. + * If the server is not run with root privileges, then the flashsocket + transport will instead listen to all new connections on the main port + for policy requests. Flash policy requests happen to both port 843 and + the destination port: + http://www.lightsphere.com/dev/articles/flash_socket_policy.html + + * [websocket test] Fix sending message to client upon connecting + * [websocket test] Fix for connection and handshake test + * [client files serving] Leverage end() write() call + * [client serving] Make sure to not do a useless file lookup when file is cached + * Finished json encoding test + * Look for the heartbeat in the decoded message + * Refactored websocket transports tests to match polling/multipart helpers + * Added coverage testing to Makefile + * Added heartbeat test to multipart + * Added buffered messages test for multipart + * Added assertions for `connected` property for all the tests + * Multipart clients tracking test + * Multipart client>server message sending test + * Make sure to only close the client stream when the roundtrip is complete + * Multipart connection and handshake tests: + - Implemented HTTP client on top of net.Stream with multipart boundary parsing for testing + - Test for connection / server>client message sending + * Removed unnecessary check for this.connection (since we now access the socket through req.connection for all transports) + * Test for `duration` parameter + * Added `make example` to Makefile + * Added clients tracking test for long polling + * Added message buffering test for long polling + * Improve this.request/this.response/this.connection + * Add 'end' listener onConnect, applies to all transports + * Improved error handling onConnect + * Remove legacy `flush` calls + * Removed unnecessary closeTimeout clearing in jsonp polling + * Make sure to close on disconnect if _open = true + * Clear disconnection timeout on disconnection (double check) + * Make sure to clear closeTimeout for polling transports on close. + * Replaced empty with null in log option + * Comma first style for client serving tests + * Long polling integration tests + * Test for heartbeat message + * Added heartbeat timeout test + * Support for listener#log false + * Corrected onConnect signature to support a request and a socket, or a request and a response. + * Removed error checking for non-upgradeable sockets, since they'll be destroyed, and error handling is done onConnect + * Added tests for websocket client tracking + * Added tests for websocket message buffering + * Make sure disconnect timeout is cleared on websocket re-connect + * Updated the flash socket with error detection, and readystate detection. + * This is needed because when a error occures we close down the connection, + * and the stream will become unwriteable. + * Also changed to a single write instead of multiple writes. + * Moved error handling to onConnect to avoid messing with the http.Server global error handlers + * Do special error handling for websocket + * Clearing heartbeat interval upon closing the connection + * Added error listeners, if theses errors are not correcly caught, they will leak memory. + * This caused http://speedo.no.de/ to go up from 1mb per connection after a ECONNECTRESET message + * Added encode=UTF-8 in jsonp-polling.js and xhr-polling.js since UTF-8 is the default encoding for http.ServerResponse.write + * Replaced string.length with Buffer.byteLength in jsonp-polling.js, listener.js and xhr-polling.js because content-length header requires number of bytes and not the number of symbols in string + * Fix COR headers/requests for different ports on Safari. + * Clearing the references to request, response and connection upon disconnect. + * Every require is blocking and requiring the sys module over and over and over again just makes no sense + it hurt performance.. Not to mention.. that it's already included. + * Socket.IO-node now serves the client out of the box for easier implementation + * Memory caching and ETag support for static files + * Tests + * Simplified demo even further thanks to new static file serving + * Failing to pass an origin header would throw an exception and crash the server. Added some handling. + * .connected renamed to ._open, and adopted proper `connected` (fixes #41) + * example/client updated to latest socket.io client + * Better checking of WebSocket connections + * Better handling of SSL location (thanks @jdub) + * Fix for cross-domain websocket (fixes #42) + * Removed clients/clientsIndex and only using the index (fixes #28) + * Fixed WebSocket location header for ws/wss (Thanks @jdub, Fixes #40) + * Cross domain issues with xhr-polling addressed. Thanks Niko Kaiser (@nicokaiser) + * Added origin verification for incoming data. + * Make sure pathname is set (thanks steadicat & swarmation team) + * Fix for accessing routes that being with the namespace but are not a connection attempt. Thanks @steadicat from swarmation + * JSONP-polling support + * Graceful closing of connection for invalid websocket clients + * Make it possible to just require 'socket.io' + * Make sure to abort the connect() method upon bad upgrade / origin verification + * Support for automatic JSON encoding/decoding + * Simplified chat example to take advantage of JSON encoding/decoding + * Removed fs sync call from example + * Better `how to use` + * Make sure to send content-type text/plain to `ok` POST responses + diff --git a/README.md b/README.md index 0db0f9556d..3bb251f428 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Socket.IO Server: Sockets for the rest of us ============================================ -The `Socket.IO` server provides seamless supports for a variety of transports intended for realtime communication +The `Socket.IO` server provides seamless support for a variety of transports intended for realtime communication. - WebSocket - WebSocket over Flash (+ XML security policy support) @@ -12,33 +12,35 @@ The `Socket.IO` server provides seamless supports for a variety of transports in ## Requirements -- Node v0.1.103+ -- [Socket.IO client](http://github.com/LearnBoost/Socket.IO) to connect from the browser +- Node v0.1.103+ with `crypto` module support (make sure you have OpenSSL + headers when installing Node to get it) +- The [Socket.IO client](http://github.com/LearnBoost/Socket.IO), to connect from the browser ## How to use -To run the demo: +To run the demo, execute the following: git clone git://github.com/LearnBoost/Socket.IO-node.git socket.io cd socket.io/example/ sudo node server.js -and point your browser to http://localhost:8080. In addition to 8080, if the transport `flashsocket` is enabled, a server will be initialized to listen to requests on the port 843. +and point your browser to `http://localhost:8080`. In addition to `8080`, if the transport `flashsocket` is enabled, a server will be initialized to listen for requests on port `843`. ### Implementing it on your project -`Socket.IO` is designed not to take over an entire port or Node `http.Server` instance. This means that if you choose your HTTP server to listen on the port 80, `socket.io` can intercept requests directed to it and the normal requests will still be served. +`Socket.IO` is designed not to take over an entire port or Node `http.Server` instance. This means that if you choose to have your HTTP server listen on port `80`, `socket.io` can intercept requests directed to it, and normal requests will still be served. -By default, the server will intercept requests that contain `socket.io` in the path / resource part of the URI. You can change this (look at the available options below). +By default, the server will intercept requests that contain `socket.io` in the path / resource part of the URI. You can change this as shown in the available options below. + +On the server: var http = require('http'), io = require('./path/to/socket.io'), server = http.createServer(function(req, res){ // your normal server code - res.writeHeader(200, {'Content-Type': 'text/html'}); - res.writeBody('

Hello world

'); - res.finish(); + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end('

Hello world

'); }); server.listen(80); @@ -52,17 +54,18 @@ By default, the server will intercept requests that contain `socket.io` in the p client.on('disconnect', function(){ … }) }); -On the client side: +On the client: -The [client side](http://github.com/learnboost/socket.io) files will be served automatically by `Socket.IO-node`. +The [client-side](http://github.com/learnboost/socket.io) files are served automatically by `Socket.IO-node`. ## Documentation @@ -76,15 +79,15 @@ Public Properties: - *server* - The instance of _process.http.Server_ + An instance of _process.http.Server_. - *options* - The passed in options combined with the defaults + The passed-in options, combined with the defaults. - *clients* - An object of clients indexed by their session ids. + An object of clients, indexed by session ID. Methods: @@ -94,11 +97,11 @@ Methods: - *removeListener(event, λ)* - Remove a listener from the listener array for the specified event. + Removes a listener from the listener array for the specified event. - *broadcast(message, [except])* - Broadcasts a message to all clients. There's an optional second argument which is an array of session ids or a single session id to avoid broadcasting to. + Broadcasts a message to all clients. Optionally, you can pass a single session ID or array of session IDs to avoid broadcasting to, as the second argument. Options: @@ -106,18 +109,19 @@ Options: socket.io - The resource is what allows the `socket.io` server to identify incoming connections by `socket.io` clients. Make sure they're in sync. + The resource is what allows the `socket.io` server to identify incoming connections from `socket.io` clients. Make sure they're in sync. - *flashPolicyServer* true - Create a Flash Policy file server on port 843 ( this is restricted port and you will need to have root permission ). If you disable the FlashPolicy file server Socket.io will automatically fall back to serving the policy file inline. + Create a Flash Policy file server on port `843` (this is restricted port and you will need to have root permission). If you disable the FlashPolicy file server, Socket.IO will automatically fall back to serving the policy file inline. - *transports* - ['websocket', 'server-events', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling'] + ['websocket', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling', + 'jsonp-polling'] A list of the accepted transports. @@ -129,21 +133,21 @@ Options: ƒ(){ sys.log } - The logging function. Defaults to outputting to stdout through `sys.log` + The logging function. Defaults to outputting to `stdout` through `sys.log` Events: - *clientConnect(client)* - Fired when a client is connected. Receives the Client instance as parameter + Fired when a client is connected. Receives the Client instance as parameter. - *clientMessage(message, client)* - Fired when a message from a client is received. Receives the message and Client instance as parameter + Fired when a message from a client is received. Receives the message and Client instance as parameters. - *clientDisconnect(client)* - Fired when a client is disconnected. Receives the Client instance as parameter + Fired when a client is disconnected. Receives the Client instance as a parameter. Important note: `this` in the event listener refers to the `Listener` instance. @@ -155,43 +159,43 @@ Public Properties: - *listener* - The `Listener` instance this client belongs to. + The `Listener` instance to which this client belongs. - *connected* - Whether the client is connected + Whether the client is connected. - *connections* - Number of times the client connected + Number of times the client has connected. Methods: - *send(message)* - Sends a message to the client + Sends a message to the client. - *broadcast(message)* - Sends a message to all other clients. Equivalent to Listener::broadcast(message, client.sessionId) + Sends a message to all other clients. Equivalent to Listener::broadcast(message, client.sessionId). ## Protocol -One of the design goals is that you should be able to implement whatever protocol you desire without `Socket.IO` getting in the way. `Socket.IO` has a minimal, unobtrusive protocol layer. It consists of two parts: +One of the design goals is that you should be able to implement whatever protocol you desire without `Socket.IO` getting in the way. `Socket.IO` has a minimal, unobtrusive protocol layer, consisting of two parts: * Connection handshake - This is required to simulate a full duplex socket with transports such as XHR Polling or Server-sent Events (which is a "one-way socket"). The basic idea is that the first message received from the server will be a JSON object that contains a session id that will be used for further communication exchanged between the client and the server. + This is required to simulate a full duplex socket with transports such as XHR Polling or Server-sent Events (which is a "one-way socket"). The basic idea is that the first message received from the server will be a JSON object that contains a session ID used for further communications exchanged between the client and server. - The concept of session also benefits naturally full-duplex WebSocket, in the event of an accidental disconnection and a quick reconnection. Messages that the server intends to deliver to the client are cached temporarily until the reconnection. + The concept of session also naturally benefits a full-duplex WebSocket, in the event of an accidental disconnection and a quick reconnection. Messages that the server intends to deliver to the client are cached temporarily until reconnection. The implementation of reconnection logic (potentially with retries) is left for the user. By default, transports that are keep-alive or open all the time (like WebSocket) have a timeout of 0 if a disconnection is detected. * Message batching - In order to optimize the resources, messages are buffered. In the event of the server trying to send multiple messages while the client is temporarily disconnected (eg: xhr polling), messages are stacked, then encoded in a lightweight way and sent to the client whenever he becomes available. + Messages are buffered in order to optimize resources. In the event of the server trying to send multiple messages while a client is temporarily disconnected (eg: xhr polling), the messages are stacked and then encoded in a lightweight way, and sent to the client whenever it becomes available. -Despite this extra layer, your messages are delivered unaltered to the different event listeners. You can JSON.stringify() objects, send XML, or maybe plain text. +Despite this extra layer, the messages are delivered unaltered to the various event listeners. You can `JSON.stringify()` objects, send XML, or even plain text. ## Credits @@ -222,4 +226,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/example/chat.html b/example/chat.html index 2c4b8b6831..1e1a5a0b17 100644 --- a/example/chat.html +++ b/example/chat.html @@ -58,4 +58,4 @@

Sample chat client

- \ No newline at end of file + diff --git a/example/server.js b/example/server.js index cd2227262c..7f10e7f346 100644 --- a/example/server.js +++ b/example/server.js @@ -1,64 +1,63 @@ -var http = require('http'), - url = require('url'), - fs = require('fs'), - io = require('../'), - sys = require('sys'), - +/** + * Important note: this application is not suitable for benchmarks! + */ + +var http = require('http') + , url = require('url') + , fs = require('fs') + , io = require('../') + , sys = require(process.binding('natives').util ? 'util' : 'sys') + , server; + server = http.createServer(function(req, res){ - // your normal server code - var path = url.parse(req.url).pathname; - switch (path){ - case '/': - res.writeHead(200, {'Content-Type': 'text/html'}); - res.write('

Welcome. Try the chat example.

'); - res.end(); - break; - - case '/json.js': - case '/chat.html': - fs.readFile(__dirname + path, function(err, data){ - if (err) return send404(res); - res.writeHead(200, {'Content-Type': path == 'json.js' ? 'text/javascript' : 'text/html'}) - res.write(data, 'utf8'); - res.end(); - }); - break; - - default: send404(res); - } + // your normal server code + var path = url.parse(req.url).pathname; + switch (path){ + case '/': + res.writeHead(200, {'Content-Type': 'text/html'}); + res.write('

Welcome. Try the chat example.

'); + res.end(); + break; + + case '/json.js': + case '/chat.html': + fs.readFile(__dirname + path, function(err, data){ + if (err) return send404(res); + res.writeHead(200, {'Content-Type': path == 'json.js' ? 'text/javascript' : 'text/html'}) + res.write(data, 'utf8'); + res.end(); + }); + break; + + default: send404(res); + } }), send404 = function(res){ - res.writeHead(404); - res.write('404'); - res.end(); + res.writeHead(404); + res.write('404'); + res.end(); }; -server.listen(8080, { - transportOptions: { - 'xhr-polling': { - closeTimeout: 1000 * 60 * 5 - } - } -}); - +server.listen(8080); + // socket.io, I choose you // simplest chat application evar -var io = io.listen(server), - buffer = []; - +var io = io.listen(server) + , buffer = []; + io.on('connection', function(client){ - client.send({ buffer: buffer }); - client.broadcast({ announcement: client.sessionId + ' connected' }); - - client.on('message', function(message){ - var msg = { message: [client.sessionId, message] }; - buffer.push(msg); - if (buffer.length > 15) buffer.shift(); - client.broadcast(msg); - }); + client.send({ buffer: buffer }); + client.broadcast({ announcement: client.sessionId + ' connected' }); + + client.on('message', function(message){ + var msg = { message: [client.sessionId, message] }; + buffer.push(msg); + if (buffer.length > 15) buffer.shift(); + client.broadcast(msg); + }); - client.on('disconnect', function(){ - client.broadcast({ announcement: client.sessionId + ' disconnected' }); - }); -}); \ No newline at end of file + client.on('disconnect', function(){ + client.broadcast({ announcement: client.sessionId + ' disconnected' }); + }); +}); diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index 4c2c92fc54..72bbf918da 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -4,7 +4,8 @@ var urlparse = require('url').parse , options = require('./utils').options , encode = require('./utils').encode , decode = require('./utils').decode - , merge = require('./utils').merge; + , merge = require('./utils').merge + , util = require(process.binding('natives').util ? 'util' : 'sys'); var Client = module.exports = function(listener, req, res, options, head){ process.EventEmitter.call(this); @@ -22,7 +23,7 @@ var Client = module.exports = function(listener, req, res, options, head){ this._onConnect(req, res); }; -require('sys').inherits(Client, process.EventEmitter); +util.inherits(Client, process.EventEmitter); Client.prototype.send = function(message){ if (!this._open || !(this.connection.readyState === 'open' || this.connection.readyState === 'writeOnly')){ @@ -47,7 +48,11 @@ Client.prototype._onMessage = function(data){ case '~h~': return this._onHeartbeat(messages[i].substr(3)); case '~j~': - messages[i] = JSON.parse(messages[i].substr(3)); + try { + messages[i] = JSON.parse(messages[i].substr(3)); + } catch(e) { + messages[i] = {}; + } break; } this.emit('message', messages[i]); @@ -160,7 +165,6 @@ Client.prototype._queue = function(message){ }; Client.prototype._generateSessionId = function(){ - if (this.sessionId) return this.listener.options.log('This client already has a session id'); this.sessionId = Math.random().toString().substr(2); return this; }; @@ -181,4 +185,4 @@ Client.prototype._verifyOrigin = function(origin){ return false; }; -for (var i in options) Client.prototype[i] = options[i]; \ No newline at end of file +for (var i in options) Client.prototype[i] = options[i]; diff --git a/lib/socket.io/listener.js b/lib/socket.io/listener.js index 3366b6132c..9bbe37e97b 100644 --- a/lib/socket.io/listener.js +++ b/lib/socket.io/listener.js @@ -1,5 +1,5 @@ var url = require('url') - , sys = require('sys') + , util = require(process.binding('natives').util ? 'util' : 'sys') , fs = require('fs') , options = require('./utils').options , Client = require('./client') @@ -14,14 +14,16 @@ var Listener = module.exports = function(server, options){ origins: '*:*', resource: 'socket.io', flashPolicyServer: true, - transports: ['websocket', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling', 'jsonp-polling'], + transports: ['websocket', 'flashsocket', 'htmlfile', 'xhr-multipart', + 'xhr-polling', 'jsonp-polling'], transportOptions: {}, - log: sys.log + log: util.log }, options); if (!this.options.log) this.options.log = function(){}; this.clients = this.clientsIndex = {}; + this._clientCount = 0; this._clientFiles = {}; var listeners = this.server.listeners('request'); @@ -40,22 +42,23 @@ var Listener = module.exports = function(server, options){ socket.destroy(); } }); - - for (var i = 0, len = this.options.transports.length; i < len; i++) { - var transport = this.options.transports[i]; - transport = transports[transport] = require('./transports/' + transport); - if ('init' in transport) transport.init(this); - } + this.options.transports.forEach(function(name) { + if (!(name in transports)) + transports[name] = require('./transports/' + name); + if ('init' in transports[name]) transports[name].init(self); + }); + this.options.log('socket.io ready - accepting connections'); }; -sys.inherits(Listener, process.EventEmitter); +util.inherits(Listener, process.EventEmitter); for (var i in options) Listener.prototype[i] = options[i]; Listener.prototype.broadcast = function(message, except){ for (var i = 0, k = Object.keys(this.clients), l = k.length; i < l; i++){ - if (this.clients[k[i]] && (!except || [].concat(except).indexOf(this.clients[k[i]].sessionId) == -1)){ + if (!except || ((typeof except == 'number' || typeof except == 'string') && k[i] != except) + || (Array.isArray(except) && except.indexOf(k[i]) == -1)){ this.clients[k[i]].send(message); } } @@ -65,20 +68,20 @@ Listener.prototype.broadcast = function(message, except){ Listener.prototype.check = function(req, res, httpUpgrade, head){ var path = url.parse(req.url).pathname, parts, cn; if (path && path.indexOf('/' + this.options.resource) === 0){ - parts = path.substr(1).split('/'); - if (this._serveClient(parts.slice(1).join('/'), req, res)) return true; - if (!(parts[1] in transports)) return false; - if (parts[2]){ - cn = this.clients[parts[2]]; + parts = path.substr(2 + this.options.resource.length).split('/'); + if (this._serveClient(parts.join('/'), req, res)) return true; + if (!(parts[0] in transports)) return false; + if (parts[1]){ + cn = this.clients[parts[1]]; if (cn){ cn._onConnect(req, res); } else { req.connection.end(); req.connection.destroy(); - this.options.log('Couldnt find client with session id "' + parts[2] + '"'); + this.options.log('Couldnt find client with session id "' + parts[1] + '"'); } } else { - this._onConnection(parts[1], req, res, httpUpgrade, head); + this._onConnection(parts[0], req, res, httpUpgrade, head); } return true; } diff --git a/lib/socket.io/transports/flashsocket.js b/lib/socket.io/transports/flashsocket.js index 75515ea6b4..8b8e052365 100644 --- a/lib/socket.io/transports/flashsocket.js +++ b/lib/socket.io/transports/flashsocket.js @@ -1,4 +1,5 @@ var net = require('net') + , util = require(process.binding('natives').util ? 'util' : 'sys') , WebSocket = require('./websocket') , listeners = [] , netserver; @@ -7,7 +8,7 @@ var Flashsocket = module.exports = function(){ WebSocket.apply(this, arguments); }; -require('sys').inherits(Flashsocket, WebSocket); +util.inherits(Flashsocket, WebSocket); Flashsocket.httpUpgrade = true; @@ -41,9 +42,10 @@ Flashsocket.init = function(listener){ netserver.listen(843); } catch(e){ if (e.errno == 13) - listener.options.log('Your node instance does not have root privileges. This means that the flash XML' - + ' policy file will be served inline instead of on port 843. This might slow down' - + ' initial connections slightly.'); + listener.options.log('Your node instance does not have root privileges.' + + 'This means that the flash XML policy file will be ' + + 'served inline instead of on port 843. This will slow ' + + 'down initial connections slightly.'); netserver = null; } } @@ -82,4 +84,4 @@ function policy(listeners) { xml += '\n'; return xml; -}; \ No newline at end of file +}; diff --git a/lib/socket.io/transports/htmlfile.js b/lib/socket.io/transports/htmlfile.js index 9812c101b7..9c4b91d1e3 100644 --- a/lib/socket.io/transports/htmlfile.js +++ b/lib/socket.io/transports/htmlfile.js @@ -1,11 +1,12 @@ var Client = require('../client') + , util = require(process.binding('natives').util ? 'util' : 'sys') , qs = require('querystring'); var HTMLFile = module.exports = function(){ Client.apply(this, arguments); }; -require('sys').inherits(HTMLFile, Client); +util.inherits(HTMLFile, Client); HTMLFile.prototype._onConnect = function(req, res){ var self = this, body = ''; @@ -43,4 +44,4 @@ HTMLFile.prototype._onConnect = function(req, res){ HTMLFile.prototype._write = function(message){ if (this._open) this.response.write(''); //json for escaping -}; \ No newline at end of file +}; diff --git a/lib/socket.io/transports/jsonp-polling.js b/lib/socket.io/transports/jsonp-polling.js index 502a9613a0..82ff6349c7 100644 --- a/lib/socket.io/transports/jsonp-polling.js +++ b/lib/socket.io/transports/jsonp-polling.js @@ -1,10 +1,11 @@ -var XHRPolling = require('./xhr-polling'); +var XHRPolling = require('./xhr-polling') + , util = require(process.binding('natives').util ? 'util' : 'sys'); JSONPPolling = module.exports = function(){ XHRPolling.apply(this, arguments); }; -require('sys').inherits(JSONPPolling, XHRPolling); +util.inherits(JSONPPolling, XHRPolling); JSONPPolling.prototype.getOptions = function(){ return { @@ -31,4 +32,4 @@ JSONPPolling.prototype._write = function(message){ this.response.end(); this._onClose(); } -}; \ No newline at end of file +}; diff --git a/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index d3798444c2..4a53ad5aba 100644 --- a/lib/socket.io/transports/websocket.js +++ b/lib/socket.io/transports/websocket.js @@ -1,22 +1,31 @@ var Client = require('../client') , Stream = require('net').Stream + , EventEmitter = require('events').EventEmitter , url = require('url') + , util = require(process.binding('natives').util ? 'util' : 'sys') , crypto = require('crypto'); WebSocket = module.exports = function(){ Client.apply(this, arguments); }; -require('sys').inherits(WebSocket, Client); +util.inherits(WebSocket, Client); WebSocket.prototype._onConnect = function(req, socket){ var self = this , headers = []; + if (!req.connection.setTimeout){ + req.connection.end(); + return false; + } + + this.parser = new Parser(); + this.parser.on('data', self._onMessage.bind(this)); + this.parser.on('error', self._onClose.bind(this)); + Client.prototype._onConnect.call(this, req); - this.data = ''; - if (this.request.headers.upgrade !== 'WebSocket' || !this._verifyOrigin(this.request.headers.origin)){ this.listener.options.log('WebSocket connection invalid or Origin not verified'); this._onClose(); @@ -26,8 +35,25 @@ WebSocket.prototype._onConnect = function(req, socket){ var origin = this.request.headers.origin, location = (origin && origin.substr(0, 5) == 'https' ? 'wss' : 'ws') + '://' + this.request.headers.host + this.request.url; - + + this.waitingForNonce = false; if ('sec-websocket-key1' in this.request.headers){ + /* We need to send the 101 response immediately when using Draft 76 with + a load balancing proxy, such as HAProxy. In order to protect an + unsuspecting non-websocket HTTP server, HAProxy will not send the + 8-byte nonce through the connection until the Upgrade: WebSocket + request has been confirmed by the WebSocket server by a 101 response + indicating that the server can handle the upgraded protocol. We + therefore must send the 101 response immediately, and then wait for + the nonce to be forwarded to us afterward in order to finish the + Draft 76 handshake. + */ + + // If we don't have the nonce yet, wait for it. + if (!(this.upgradeHead && this.upgradeHead.length >= 8)) { + this.waitingForNonce = true; + } + headers = [ 'HTTP/1.1 101 WebSocket Protocol Handshake', 'Upgrade: WebSocket', @@ -48,39 +74,50 @@ WebSocket.prototype._onConnect = function(req, socket){ 'WebSocket-Location: ' + location ]; - try { - this.connection.write(headers.concat('', '').join('\r\n')); - } catch(e){ - this._onClose(); - } + } + + try { + this.connection.write(headers.concat('', '').join('\r\n')); + } catch(e){ + this._onClose(); } this.connection.setTimeout(0); this.connection.setNoDelay(true); this.connection.setEncoding('utf-8'); + if (this.waitingForNonce) { + // Since we will be receiving the binary nonce through the normal HTTP + // data event, set the connection to 'binary' temporarily + this.connection.setEncoding('binary'); + this._headers = headers; + } + else { + if (this._proveReception(headers)) this._payload(); + } + + this.buffer = ""; + this.connection.addListener('data', function(data){ - self._handle(data); + self.buffer += data; + if (self.waitingForNonce) { + if (self.buffer.length < 8) { return; } + // Restore the connection to utf8 encoding after receiving the nonce + self.connection.setEncoding('utf8'); + self.waitingForNonce = false; + // Stuff the nonce into the location where it's expected to be + self.upgradeHead = self.buffer.substr(0,8); + self.buffer = self.buffer.substr(8); + if (self.buffer.length > 0) { + self.parser.add(self.buffer); + } + if (self._proveReception(self._headers)) { self._payload(); } + return; + } + + self.parser.add(data); }); - if (this._proveReception(headers)) this._payload(); -}; - -WebSocket.prototype._handle = function(data){ - var chunk, chunks, chunk_count; - this.data += data; - chunks = this.data.split('\ufffd'); - chunk_count = chunks.length - 1; - for (var i = 0; i < chunk_count; i++){ - chunk = chunks[i]; - if (chunk[0] !== '\u0000'){ - this.listener.options.log('Data incorrectly framed by UA. Dropping connection'); - this._onClose(); - return false; - } - this._onMessage(chunk.slice(1)); - } - this.data = chunks[chunks.length - 1]; }; // http://www.whatwg.org/specs/web-apps/current-work/complete/network.html#opening-handshake @@ -114,7 +151,7 @@ WebSocket.prototype._proveReception = function(headers){ md5.update(this.upgradeHead.toString('binary')); try { - this.connection.write(headers.concat('', '').join('\r\n') + md5.digest('binary'), 'binary'); + this.connection.write(md5.digest('binary'), 'binary'); } catch(e){ this._onClose(); } @@ -133,4 +170,41 @@ WebSocket.prototype._write = function(message){ } }; -WebSocket.httpUpgrade = true; \ No newline at end of file +WebSocket.httpUpgrade = true; + +function Parser(){ + this.buffer = ''; + this.i = 0; +}; + +Parser.prototype.__proto__ = EventEmitter.prototype; + +Parser.prototype.add = function(data){ + this.buffer += data; + this.parse(); +}; + +Parser.prototype.parse = function(){ + for (var i = this.i, chr, l = this.buffer.length; i < l; i++){ + chr = this.buffer[i]; + if (i === 0){ + if (chr != '\u0000') + this.error('Bad framing. Expected null byte as first frame'); + else + continue; + } + if (chr == '\ufffd'){ + this.emit('data', this.buffer.substr(1, this.buffer.length - 2)); + this.buffer = this.buffer.substr(i + 1); + this.i = 0; + return this.parse(); + } + } +}; + +Parser.prototype.error = function(reason){ + this.buffer = ''; + this.i = 0; + this.emit('error', reason); + return this; +}; diff --git a/lib/socket.io/transports/xhr-multipart.js b/lib/socket.io/transports/xhr-multipart.js index 1c2e9c7253..e080ea3d7f 100644 --- a/lib/socket.io/transports/xhr-multipart.js +++ b/lib/socket.io/transports/xhr-multipart.js @@ -1,11 +1,12 @@ var Client = require('../client') + , util = require(process.binding('natives').util ? 'util' : 'sys') , qs = require('querystring'); var Multipart = module.exports = function(){ Client.apply(this, arguments); }; -require('sys').inherits(Multipart, Client); +util.inherits(Multipart, Client); Multipart.prototype._onConnect = function(req, res){ var self = this, body = '', headers = {}; @@ -61,4 +62,4 @@ Multipart.prototype._write = function(message){ this.response.write(message + "\n"); this.response.write("--socketio\n"); } -}; \ No newline at end of file +}; diff --git a/lib/socket.io/transports/xhr-polling.js b/lib/socket.io/transports/xhr-polling.js index 13433b6945..112534c741 100644 --- a/lib/socket.io/transports/xhr-polling.js +++ b/lib/socket.io/transports/xhr-polling.js @@ -1,22 +1,24 @@ var Client = require('../client') + , util = require(process.binding('natives').util ? 'util' : 'sys') , qs = require('querystring'); var Polling = module.exports = function(){ Client.apply(this, arguments); }; -require('sys').inherits(Polling, Client); +util.inherits(Polling, Client); Polling.prototype.getOptions = function(){ return { timeout: null, // no heartbeats closeTimeout: 8000, - duration: 20000 + duration: 50000 }; }; Polling.prototype._onConnect = function(req, res){ var self = this, body = ''; + switch (req.method){ case 'GET': Client.prototype._onConnect.apply(this, [req, res]); @@ -74,4 +76,4 @@ Polling.prototype._write = function(message){ this.response.end(); this._onClose(); } -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index 836b7407d1..d072c31fa0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name" : "socket.io" , "description" : "The cross-browser WebSocket" -, "version" : "0.6.1" +, "version" : "0.6.3" , "author" : "LearnBoost" , "licenses" : [ { "type" : "MIT" diff --git a/support/socket.io-client/lib/io.js b/support/socket.io-client/lib/io.js index 2c20fd3915..cfc59f9557 100644 --- a/support/socket.io-client/lib/io.js +++ b/support/socket.io-client/lib/io.js @@ -7,12 +7,12 @@ */ this.io = { - version: '0.6', + version: '0.6.1', setPath: function(path){ if (window.console && console.error) console.error('io.setPath will be removed. Please set the variable WEB_SOCKET_SWF_LOCATION pointing to WebSocketMain.swf'); this.path = /\/$/.test(path) ? path : path + '/'; - WEB_SOCKET_SWF_LOCATION = path + 'lib/vendor/web-socket-js/WebSocketMain.swf'; + WEB_SOCKET_SWF_LOCATION = path + 'lib/vendor/web-socket-js/WebSocketMain.swf'; } }; @@ -20,5 +20,6 @@ if ('jQuery' in this) jQuery.io = this.io; if (typeof window != 'undefined'){ // WEB_SOCKET_SWF_LOCATION = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//cdn.socket.io/' + this.io.version + '/WebSocketMain.swf'; - WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf'; -} \ No newline at end of file + if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') + WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf'; +} diff --git a/support/socket.io-client/lib/socket.js b/support/socket.io-client/lib/socket.js index f98fa3d59c..1e113ddacb 100644 --- a/support/socket.io-client/lib/socket.js +++ b/support/socket.io-client/lib/socket.js @@ -62,7 +62,7 @@ this.transport.connect(); if (this.options.connectTimeout){ var self = this; - setTimeout(function(){ + this.connectTimeoutTimer = setTimeout(function(){ if (!self.connected){ self.disconnect(); if (self.options.tryTransportsOnConnectTimeout && !self._rememberedTransport){ @@ -89,6 +89,7 @@ }; Socket.prototype.disconnect = function(){ + if (this.connectTimeoutTimer) clearTimeout(this.connectTimeoutTimer); this.transport.disconnect(); return this; }; @@ -99,15 +100,15 @@ return this; }; - Socket.prototype.fire = function(name, args){ - if (name in this._events){ - var events = this._events[name].concat(); - for (var i = 0, ii = events.length; i < ii; i++) - events[i].apply(this, args === undefined ? [] : args); - } - return this; - }; - + Socket.prototype.emit = function(name, args){ + if (name in this._events){ + var events = this._events[name].concat(); + for (var i = 0, ii = events.length; i < ii; i++) + events[i].apply(this, args === undefined ? [] : args); + } + return this; + }; + Socket.prototype.removeEvent = function(name, fn){ if (name in this._events){ for (var a = 0, l = this._events[name].length; a < l; a++) @@ -138,11 +139,11 @@ this.connecting = false; this._doQueue(); if (this.options.rememberTransport) this.options.document.cookie = 'socketio=' + encodeURIComponent(this.transport.type); - this.fire('connect'); + this.emit('connect'); }; Socket.prototype._onMessage = function(data){ - this.fire('message', [data]); + this.emit('message', [data]); }; Socket.prototype._onDisconnect = function(){ @@ -150,8 +151,10 @@ this.connected = false; this.connecting = false; this._queueStack = []; - if (wasConnected) this.fire('disconnect'); + if (wasConnected) this.emit('disconnect'); }; + + Socket.prototype.fire = Socket.prototype.emit; Socket.prototype.addListener = Socket.prototype.addEvent = Socket.prototype.addEventListener = Socket.prototype.on; diff --git a/support/socket.io-client/lib/transports/websocket.js b/support/socket.io-client/lib/transports/websocket.js index f4d2bd345e..49de42ee57 100644 --- a/support/socket.io-client/lib/transports/websocket.js +++ b/support/socket.io-client/lib/transports/websocket.js @@ -21,6 +21,7 @@ this.socket = new WebSocket(this._prepareUrl()); this.socket.onmessage = function(ev){ self._onData(ev.data); }; this.socket.onclose = function(ev){ self._onClose(); }; + this.socket.onerror = function(e){ self._onError(e); }; return this; }; @@ -38,6 +39,10 @@ this._onDisconnect(); return this; }; + + WS.prototype._onError = function(e){ + this.base.emit('error', [e]); + }; WS.prototype._prepareUrl = function(){ return (this.base.options.secure ? 'wss' : 'ws') @@ -57,4 +62,4 @@ return true; }; -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/lib/transports/xhr-multipart.js b/support/socket.io-client/lib/transports/xhr-multipart.js index ef850b4011..7e2acca3fb 100644 --- a/support/socket.io-client/lib/transports/xhr-multipart.js +++ b/support/socket.io-client/lib/transports/xhr-multipart.js @@ -22,7 +22,7 @@ this._xhr.onreadystatechange = function(){ if (self._xhr.readyState == 3) self._onData(self._xhr.responseText); }; - this._xhr.send(); + this._xhr.send(null); }; XHRMultipart.check = function(){ @@ -33,4 +33,4 @@ return true; }; -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/lib/transports/xhr-polling.js b/support/socket.io-client/lib/transports/xhr-polling.js index 17542672ca..6e065687a9 100644 --- a/support/socket.io-client/lib/transports/xhr-polling.js +++ b/support/socket.io-client/lib/transports/xhr-polling.js @@ -34,27 +34,20 @@ XHRPolling.prototype._get = function(){ var self = this; this._xhr = this._request(+ new Date, 'GET'); - if ('onload' in this._xhr){ - this._xhr.onload = function(){ - self._onData(this.responseText); - self._get(); - }; - } else { - this._xhr.onreadystatechange = function(){ - var status; - if (self._xhr.readyState == 4){ - self._xhr.onreadystatechange = empty; - try { status = self._xhr.status; } catch(e){} - if (status == 200){ - self._onData(self._xhr.responseText); - self._get(); - } else { - self._onDisconnect(); - } - } - }; - } - this._xhr.send(); + this._xhr.onreadystatechange = function(){ + var status; + if (self._xhr.readyState == 4){ + self._xhr.onreadystatechange = empty; + try { status = self._xhr.status; } catch(e){} + if (status == 200){ + self._onData(self._xhr.responseText); + self._get(); + } else { + self._onDisconnect(); + } + } + }; + this._xhr.send(null); }; XHRPolling.check = function(){ @@ -65,4 +58,4 @@ return io.Transport.XHR.xdomainCheck(); }; -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/lib/transports/xhr.js b/support/socket.io-client/lib/transports/xhr.js index 041ab11819..db866e114f 100644 --- a/support/socket.io-client/lib/transports/xhr.js +++ b/support/socket.io-client/lib/transports/xhr.js @@ -92,13 +92,17 @@ XHR.prototype._onDisconnect = function(){ if (this._xhr){ - this._xhr.onreadystatechange = this._xhr.onload = empty; - this._xhr.abort(); + this._xhr.onreadystatechange = empty; + try { + this._xhr.abort(); + } catch(e){} this._xhr = null; } if (this._sendXhr){ - this._sendXhr.onreadystatechange = this._sendXhr.onload = empty; - this._sendXhr.abort(); + this._sendXhr.onreadystatechange = empty; + try { + this._sendXhr.abort(); + } catch(e){} this._sendXhr = null; } this._sendBuffer = []; @@ -128,4 +132,4 @@ XHR.request = request; -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/lib/util.js b/support/socket.io-client/lib/util.js index 91abd2e35a..1e5850a31e 100644 --- a/support/socket.io-client/lib/util.js +++ b/support/socket.io-client/lib/util.js @@ -15,7 +15,7 @@ ios: false, load: function(fn){ - if (document.readyState == 'complete' || _pageLoaded) return fn(); + if (/loaded|complete/.test(document.readyState) || _pageLoaded) return fn(); if ('attachEvent' in window){ window.attachEvent('onload', fn); } else { @@ -57,4 +57,4 @@ _pageLoaded = true; }); -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/socket.io.js b/support/socket.io-client/socket.io.js index e9e216ba07..3c6eaccda8 100644 --- a/support/socket.io-client/socket.io.js +++ b/support/socket.io-client/socket.io.js @@ -1,4 +1,4 @@ -/** Socket.IO 0.6 - Built with build.js */ +/** Socket.IO 0.6.1 - Built with build.js */ /** * Socket.IO client * @@ -8,12 +8,12 @@ */ this.io = { - version: '0.6', + version: '0.6.1', setPath: function(path){ if (window.console && console.error) console.error('io.setPath will be removed. Please set the variable WEB_SOCKET_SWF_LOCATION pointing to WebSocketMain.swf'); this.path = /\/$/.test(path) ? path : path + '/'; - WEB_SOCKET_SWF_LOCATION = path + 'lib/vendor/web-socket-js/WebSocketMain.swf'; + WEB_SOCKET_SWF_LOCATION = path + 'lib/vendor/web-socket-js/WebSocketMain.swf'; } }; @@ -21,8 +21,10 @@ if ('jQuery' in this) jQuery.io = this.io; if (typeof window != 'undefined'){ // WEB_SOCKET_SWF_LOCATION = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//cdn.socket.io/' + this.io.version + '/WebSocketMain.swf'; - WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf'; + if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') + WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf'; } + /** * Socket.IO client * @@ -40,7 +42,7 @@ if (typeof window != 'undefined'){ ios: false, load: function(fn){ - if (document.readyState == 'complete' || _pageLoaded) return fn(); + if (/loaded|complete/.test(document.readyState) || _pageLoaded) return fn(); if ('attachEvent' in window){ window.attachEvent('onload', fn); } else { @@ -83,6 +85,7 @@ if (typeof window != 'undefined'){ }); })(); + /** * Socket.IO client * @@ -318,13 +321,17 @@ if (typeof window != 'undefined'){ XHR.prototype._onDisconnect = function(){ if (this._xhr){ - this._xhr.onreadystatechange = this._xhr.onload = empty; - this._xhr.abort(); + this._xhr.onreadystatechange = empty; + try { + this._xhr.abort(); + } catch(e){} this._xhr = null; } if (this._sendXhr){ - this._sendXhr.onreadystatechange = this._sendXhr.onload = empty; - this._sendXhr.abort(); + this._sendXhr.onreadystatechange = empty; + try { + this._sendXhr.abort(); + } catch(e){} this._sendXhr = null; } this._sendBuffer = []; @@ -355,6 +362,7 @@ if (typeof window != 'undefined'){ XHR.request = request; })(); + /** * Socket.IO client * @@ -378,6 +386,7 @@ if (typeof window != 'undefined'){ this.socket = new WebSocket(this._prepareUrl()); this.socket.onmessage = function(ev){ self._onData(ev.data); }; this.socket.onclose = function(ev){ self._onClose(); }; + this.socket.onerror = function(e){ self._onError(e); }; return this; }; @@ -395,6 +404,10 @@ if (typeof window != 'undefined'){ this._onDisconnect(); return this; }; + + WS.prototype._onError = function(e){ + this.base.emit('error', [e]); + }; WS.prototype._prepareUrl = function(){ return (this.base.options.secure ? 'wss' : 'ws') @@ -415,6 +428,7 @@ if (typeof window != 'undefined'){ }; })(); + /** * Socket.IO client * @@ -565,7 +579,7 @@ if (typeof window != 'undefined'){ this._xhr.onreadystatechange = function(){ if (self._xhr.readyState == 3) self._onData(self._xhr.responseText); }; - this._xhr.send(); + this._xhr.send(null); }; XHRMultipart.check = function(){ @@ -577,6 +591,7 @@ if (typeof window != 'undefined'){ }; })(); + /** * Socket.IO client * @@ -613,27 +628,20 @@ if (typeof window != 'undefined'){ XHRPolling.prototype._get = function(){ var self = this; this._xhr = this._request(+ new Date, 'GET'); - if ('onload' in this._xhr){ - this._xhr.onload = function(){ - self._onData(this.responseText); - self._get(); - }; - } else { - this._xhr.onreadystatechange = function(){ - var status; - if (self._xhr.readyState == 4){ - self._xhr.onreadystatechange = empty; - try { status = self._xhr.status; } catch(e){} - if (status == 200){ - self._onData(self._xhr.responseText); - self._get(); - } else { - self._onDisconnect(); - } - } - }; - } - this._xhr.send(); + this._xhr.onreadystatechange = function(){ + var status; + if (self._xhr.readyState == 4){ + self._xhr.onreadystatechange = empty; + try { status = self._xhr.status; } catch(e){} + if (status == 200){ + self._onData(self._xhr.responseText); + self._get(); + } else { + self._onDisconnect(); + } + } + }; + this._xhr.send(null); }; XHRPolling.check = function(){ @@ -645,6 +653,7 @@ if (typeof window != 'undefined'){ }; })(); + /** * Socket.IO client * @@ -825,7 +834,7 @@ JSONPPolling.xdomainCheck = function(){ this.transport.connect(); if (this.options.connectTimeout){ var self = this; - setTimeout(function(){ + this.connectTimeoutTimer = setTimeout(function(){ if (!self.connected){ self.disconnect(); if (self.options.tryTransportsOnConnectTimeout && !self._rememberedTransport){ @@ -852,6 +861,7 @@ JSONPPolling.xdomainCheck = function(){ }; Socket.prototype.disconnect = function(){ + if (this.connectTimeoutTimer) clearTimeout(this.connectTimeoutTimer); this.transport.disconnect(); return this; }; @@ -862,15 +872,15 @@ JSONPPolling.xdomainCheck = function(){ return this; }; - Socket.prototype.fire = function(name, args){ - if (name in this._events){ - var events = this._events[name].concat(); - for (var i = 0, ii = events.length; i < ii; i++) - events[i].apply(this, args === undefined ? [] : args); - } - return this; - }; - + Socket.prototype.emit = function(name, args){ + if (name in this._events){ + var events = this._events[name].concat(); + for (var i = 0, ii = events.length; i < ii; i++) + events[i].apply(this, args === undefined ? [] : args); + } + return this; + }; + Socket.prototype.removeEvent = function(name, fn){ if (name in this._events){ for (var a = 0, l = this._events[name].length; a < l; a++) @@ -901,11 +911,11 @@ JSONPPolling.xdomainCheck = function(){ this.connecting = false; this._doQueue(); if (this.options.rememberTransport) this.options.document.cookie = 'socketio=' + encodeURIComponent(this.transport.type); - this.fire('connect'); + this.emit('connect'); }; Socket.prototype._onMessage = function(data){ - this.fire('message', [data]); + this.emit('message', [data]); }; Socket.prototype._onDisconnect = function(){ @@ -913,620 +923,623 @@ JSONPPolling.xdomainCheck = function(){ this.connected = false; this.connecting = false; this._queueStack = []; - if (wasConnected) this.fire('disconnect'); + if (wasConnected) this.emit('disconnect'); }; + + Socket.prototype.fire = Socket.prototype.emit; Socket.prototype.addListener = Socket.prototype.addEvent = Socket.prototype.addEventListener = Socket.prototype.on; })(); + /* SWFObject v2.2 is released under the MIT License */ var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab 0) { - for (var i = 0; i < ol; i++) { - if (typeof objects[i].SetVariable != "undefined") { - activeObjects[activeObjects.length] = objects[i]; - } - } - } - var embeds = document.getElementsByTagName("embed"); - var el = embeds.length; - var activeEmbeds = []; - if (el > 0) { - for (var j = 0; j < el; j++) { - if (typeof embeds[j].SetVariable != "undefined") { - activeEmbeds[activeEmbeds.length] = embeds[j]; - } - } - } - var aol = activeObjects.length; - var ael = activeEmbeds.length; - var searchStr = "bridgeName="+ bridgeName; - if ((aol == 1 && !ael) || (aol == 1 && ael == 1)) { - FABridge.attachBridge(activeObjects[0], bridgeName); - } - else if (ael == 1 && !aol) { - FABridge.attachBridge(activeEmbeds[0], bridgeName); - } - else { - var flash_found = false; - if (aol > 1) { - for (var k = 0; k < aol; k++) { - var params = activeObjects[k].childNodes; - for (var l = 0; l < params.length; l++) { - var param = params[l]; - if (param.nodeType == 1 && param.tagName.toLowerCase() == "param" && param["name"].toLowerCase() == "flashvars" && param["value"].indexOf(searchStr) >= 0) { - FABridge.attachBridge(activeObjects[k], bridgeName); - flash_found = true; - break; - } - } - if (flash_found) { - break; - } - } - } - if (!flash_found && ael > 1) { - for (var m = 0; m < ael; m++) { - var flashVars = activeEmbeds[m].attributes.getNamedItem("flashVars").nodeValue; - if (flashVars.indexOf(searchStr) >= 0) { - FABridge.attachBridge(activeEmbeds[m], bridgeName); - break; - } - } - } - } - return true; -} - -// used to track multiple bridge instances, since callbacks from AS are global across the page. - -FABridge.nextBridgeID = 0; -FABridge.instances = {}; -FABridge.idMap = {}; -FABridge.refCount = 0; - -FABridge.extractBridgeFromID = function(id) -{ - var bridgeID = (id >> 16); - return FABridge.idMap[bridgeID]; -} - -FABridge.attachBridge = function(instance, bridgeName) -{ - var newBridgeInstance = new FABridge(instance, bridgeName); - - FABridge[bridgeName] = newBridgeInstance; - -/* FABridge[bridgeName] = function() { - return newBridgeInstance.root(); - } -*/ - var callbacks = FABridge.initCallbacks[bridgeName]; - if (callbacks == null) - { - return; - } - for (var i = 0; i < callbacks.length; i++) - { - callbacks[i].call(newBridgeInstance); - } - delete FABridge.initCallbacks[bridgeName] -} - -// some methods can't be proxied. You can use the explicit get,set, and call methods if necessary. - -FABridge.blockedMethods = -{ - toString: true, - get: true, - set: true, - call: true -}; - -FABridge.prototype = -{ - - -// bootstrapping - - root: function() - { - return this.deserialize(this.target.getRoot()); - }, -//clears all of the AS objects in the cache maps - releaseASObjects: function() - { - return this.target.releaseASObjects(); - }, -//clears a specific object in AS from the type maps - releaseNamedASObject: function(value) - { - if(typeof(value) != "object") - { - return false; - } - else - { - var ret = this.target.releaseNamedASObject(value.fb_instance_id); - return ret; - } - }, -//create a new AS Object - create: function(className) - { - return this.deserialize(this.target.create(className)); - }, - - - // utilities - - makeID: function(token) - { - return (this.bridgeID << 16) + token; - }, - - - // low level access to the flash object - -//get a named property from an AS object - getPropertyFromAS: function(objRef, propName) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - retVal = this.target.getPropFromAS(objRef, propName); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, -//set a named property on an AS object - setPropertyInAS: function(objRef,propName, value) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - retVal = this.target.setPropInAS(objRef,propName, this.serialize(value)); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, - -//call an AS function - callASFunction: function(funcID, args) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - retVal = this.target.invokeASFunction(funcID, this.serialize(args)); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, -//call a method on an AS object - callASMethod: function(objID, funcName, args) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - args = this.serialize(args); - retVal = this.target.invokeASMethod(objID, funcName, args); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, - - // responders to remote calls from flash - - //callback from flash that executes a local JS function - //used mostly when setting js functions as callbacks on events - invokeLocalFunction: function(funcID, args) - { - var result; - var func = this.localFunctionCache[funcID]; - - if(func != undefined) - { - result = this.serialize(func.apply(null, this.deserialize(args))); - } - - return result; - }, - - // Object Types and Proxies - - // accepts an object reference, returns a type object matching the obj reference. - getTypeFromName: function(objTypeName) - { - return this.remoteTypeCache[objTypeName]; - }, - //create an AS proxy for the given object ID and type - createProxy: function(objID, typeName) - { - var objType = this.getTypeFromName(typeName); - instanceFactory.prototype = objType; - var instance = new instanceFactory(objID); - this.remoteInstanceCache[objID] = instance; - return instance; - }, - //return the proxy associated with the given object ID - getProxy: function(objID) - { - return this.remoteInstanceCache[objID]; - }, - - // accepts a type structure, returns a constructed type - addTypeDataToCache: function(typeData) - { - var newType = new ASProxy(this, typeData.name); - var accessors = typeData.accessors; - for (var i = 0; i < accessors.length; i++) - { - this.addPropertyToType(newType, accessors[i]); - } - - var methods = typeData.methods; - for (var i = 0; i < methods.length; i++) - { - if (FABridge.blockedMethods[methods[i]] == undefined) - { - this.addMethodToType(newType, methods[i]); - } - } - - - this.remoteTypeCache[newType.typeName] = newType; - return newType; - }, - - //add a property to a typename; used to define the properties that can be called on an AS proxied object - addPropertyToType: function(ty, propName) - { - var c = propName.charAt(0); - var setterName; - var getterName; - if(c >= "a" && c <= "z") - { - getterName = "get" + c.toUpperCase() + propName.substr(1); - setterName = "set" + c.toUpperCase() + propName.substr(1); - } - else - { - getterName = "get" + propName; - setterName = "set" + propName; - } - ty[setterName] = function(val) - { - this.bridge.setPropertyInAS(this.fb_instance_id, propName, val); - } - ty[getterName] = function() - { - return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); - } - }, - - //add a method to a typename; used to define the methods that can be callefd on an AS proxied object - addMethodToType: function(ty, methodName) - { - ty[methodName] = function() - { - return this.bridge.deserialize(this.bridge.callASMethod(this.fb_instance_id, methodName, FABridge.argsToArray(arguments))); - } - }, - - // Function Proxies - - //returns the AS proxy for the specified function ID - getFunctionProxy: function(funcID) - { - var bridge = this; - if (this.remoteFunctionCache[funcID] == null) - { - this.remoteFunctionCache[funcID] = function() - { - bridge.callASFunction(funcID, FABridge.argsToArray(arguments)); - } - } - return this.remoteFunctionCache[funcID]; - }, - - //reutrns the ID of the given function; if it doesnt exist it is created and added to the local cache - getFunctionID: function(func) - { - if (func.__bridge_id__ == undefined) - { - func.__bridge_id__ = this.makeID(this.nextLocalFuncID++); - this.localFunctionCache[func.__bridge_id__] = func; - } - return func.__bridge_id__; - }, - - // serialization / deserialization - - serialize: function(value) - { - var result = {}; - - var t = typeof(value); - //primitives are kept as such - if (t == "number" || t == "string" || t == "boolean" || t == null || t == undefined) - { - result = value; - } - else if (value instanceof Array) - { - //arrays are serializesd recursively - result = []; - for (var i = 0; i < value.length; i++) - { - result[i] = this.serialize(value[i]); - } - } - else if (t == "function") - { - //js functions are assigned an ID and stored in the local cache - result.type = FABridge.TYPE_JSFUNCTION; - result.value = this.getFunctionID(value); - } - else if (value instanceof ASProxy) - { - result.type = FABridge.TYPE_ASINSTANCE; - result.value = value.fb_instance_id; - } - else - { - result.type = FABridge.TYPE_ANONYMOUS; - result.value = value; - } - - return result; - }, - - //on deserialization we always check the return for the specific error code that is used to marshall NPE's into JS errors - // the unpacking is done by returning the value on each pachet for objects/arrays - deserialize: function(packedValue) - { - - var result; - - var t = typeof(packedValue); - if (t == "number" || t == "string" || t == "boolean" || packedValue == null || packedValue == undefined) - { - result = this.handleError(packedValue); - } - else if (packedValue instanceof Array) - { - result = []; - for (var i = 0; i < packedValue.length; i++) - { - result[i] = this.deserialize(packedValue[i]); - } - } - else if (t == "object") - { - for(var i = 0; i < packedValue.newTypes.length; i++) - { - this.addTypeDataToCache(packedValue.newTypes[i]); - } - for (var aRefID in packedValue.newRefs) - { - this.createProxy(aRefID, packedValue.newRefs[aRefID]); - } - if (packedValue.type == FABridge.TYPE_PRIMITIVE) - { - result = packedValue.value; - } - else if (packedValue.type == FABridge.TYPE_ASFUNCTION) - { - result = this.getFunctionProxy(packedValue.value); - } - else if (packedValue.type == FABridge.TYPE_ASINSTANCE) - { - result = this.getProxy(packedValue.value); - } - else if (packedValue.type == FABridge.TYPE_ANONYMOUS) - { - result = packedValue.value; - } - } - return result; - }, - //increases the reference count for the given object - addRef: function(obj) - { - this.target.incRef(obj.fb_instance_id); - }, - //decrease the reference count for the given object and release it if needed - release:function(obj) - { - this.target.releaseRef(obj.fb_instance_id); - }, - - // check the given value for the components of the hard-coded error code : __FLASHERROR - // used to marshall NPE's into flash - - handleError: function(value) - { - if (typeof(value)=="string" && value.indexOf("__FLASHERROR")==0) - { - var myErrorMessage = value.split("||"); - if(FABridge.refCount > 0 ) - { - FABridge.refCount--; - } - throw new Error(myErrorMessage[1]); - return value; - } - else - { - return value; - } - } -}; - -// The root ASProxy class that facades a flash object - -ASProxy = function(bridge, typeName) -{ - this.bridge = bridge; - this.typeName = typeName; - return this; -}; -//methods available on each ASProxy object -ASProxy.prototype = -{ - get: function(propName) - { - return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); - }, - - set: function(propName, value) - { - this.bridge.setPropertyInAS(this.fb_instance_id, propName, value); - }, - - call: function(funcName, args) - { - this.bridge.callASMethod(this.fb_instance_id, funcName, args); - }, - - addRef: function() { - this.bridge.addRef(this); - }, - - release: function() { - this.bridge.release(this); - } -}; +/* +/* +Copyright 2006 Adobe Systems Incorporated + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + + +/* + * The Bridge class, responsible for navigating AS instances + */ +function FABridge(target,bridgeName) +{ + this.target = target; + this.remoteTypeCache = {}; + this.remoteInstanceCache = {}; + this.remoteFunctionCache = {}; + this.localFunctionCache = {}; + this.bridgeID = FABridge.nextBridgeID++; + this.name = bridgeName; + this.nextLocalFuncID = 0; + FABridge.instances[this.name] = this; + FABridge.idMap[this.bridgeID] = this; + + return this; +} + +// type codes for packed values +FABridge.TYPE_ASINSTANCE = 1; +FABridge.TYPE_ASFUNCTION = 2; + +FABridge.TYPE_JSFUNCTION = 3; +FABridge.TYPE_ANONYMOUS = 4; + +FABridge.initCallbacks = {}; +FABridge.userTypes = {}; + +FABridge.addToUserTypes = function() +{ + for (var i = 0; i < arguments.length; i++) + { + FABridge.userTypes[arguments[i]] = { + 'typeName': arguments[i], + 'enriched': false + }; + } +} + +FABridge.argsToArray = function(args) +{ + var result = []; + for (var i = 0; i < args.length; i++) + { + result[i] = args[i]; + } + return result; +} + +function instanceFactory(objID) +{ + this.fb_instance_id = objID; + return this; +} + +function FABridge__invokeJSFunction(args) +{ + var funcID = args[0]; + var throughArgs = args.concat();//FABridge.argsToArray(arguments); + throughArgs.shift(); + + var bridge = FABridge.extractBridgeFromID(funcID); + return bridge.invokeLocalFunction(funcID, throughArgs); +} + +FABridge.addInitializationCallback = function(bridgeName, callback) +{ + var inst = FABridge.instances[bridgeName]; + if (inst != undefined) + { + callback.call(inst); + return; + } + + var callbackList = FABridge.initCallbacks[bridgeName]; + if(callbackList == null) + { + FABridge.initCallbacks[bridgeName] = callbackList = []; + } + + callbackList.push(callback); +} + +// updated for changes to SWFObject2 +function FABridge__bridgeInitialized(bridgeName) { + var objects = document.getElementsByTagName("object"); + var ol = objects.length; + var activeObjects = []; + if (ol > 0) { + for (var i = 0; i < ol; i++) { + if (typeof objects[i].SetVariable != "undefined") { + activeObjects[activeObjects.length] = objects[i]; + } + } + } + var embeds = document.getElementsByTagName("embed"); + var el = embeds.length; + var activeEmbeds = []; + if (el > 0) { + for (var j = 0; j < el; j++) { + if (typeof embeds[j].SetVariable != "undefined") { + activeEmbeds[activeEmbeds.length] = embeds[j]; + } + } + } + var aol = activeObjects.length; + var ael = activeEmbeds.length; + var searchStr = "bridgeName="+ bridgeName; + if ((aol == 1 && !ael) || (aol == 1 && ael == 1)) { + FABridge.attachBridge(activeObjects[0], bridgeName); + } + else if (ael == 1 && !aol) { + FABridge.attachBridge(activeEmbeds[0], bridgeName); + } + else { + var flash_found = false; + if (aol > 1) { + for (var k = 0; k < aol; k++) { + var params = activeObjects[k].childNodes; + for (var l = 0; l < params.length; l++) { + var param = params[l]; + if (param.nodeType == 1 && param.tagName.toLowerCase() == "param" && param["name"].toLowerCase() == "flashvars" && param["value"].indexOf(searchStr) >= 0) { + FABridge.attachBridge(activeObjects[k], bridgeName); + flash_found = true; + break; + } + } + if (flash_found) { + break; + } + } + } + if (!flash_found && ael > 1) { + for (var m = 0; m < ael; m++) { + var flashVars = activeEmbeds[m].attributes.getNamedItem("flashVars").nodeValue; + if (flashVars.indexOf(searchStr) >= 0) { + FABridge.attachBridge(activeEmbeds[m], bridgeName); + break; + } + } + } + } + return true; +} + +// used to track multiple bridge instances, since callbacks from AS are global across the page. + +FABridge.nextBridgeID = 0; +FABridge.instances = {}; +FABridge.idMap = {}; +FABridge.refCount = 0; + +FABridge.extractBridgeFromID = function(id) +{ + var bridgeID = (id >> 16); + return FABridge.idMap[bridgeID]; +} + +FABridge.attachBridge = function(instance, bridgeName) +{ + var newBridgeInstance = new FABridge(instance, bridgeName); + + FABridge[bridgeName] = newBridgeInstance; + +/* FABridge[bridgeName] = function() { + return newBridgeInstance.root(); + } +*/ + var callbacks = FABridge.initCallbacks[bridgeName]; + if (callbacks == null) + { + return; + } + for (var i = 0; i < callbacks.length; i++) + { + callbacks[i].call(newBridgeInstance); + } + delete FABridge.initCallbacks[bridgeName] +} + +// some methods can't be proxied. You can use the explicit get,set, and call methods if necessary. + +FABridge.blockedMethods = +{ + toString: true, + get: true, + set: true, + call: true +}; + +FABridge.prototype = +{ + + +// bootstrapping + + root: function() + { + return this.deserialize(this.target.getRoot()); + }, +//clears all of the AS objects in the cache maps + releaseASObjects: function() + { + return this.target.releaseASObjects(); + }, +//clears a specific object in AS from the type maps + releaseNamedASObject: function(value) + { + if(typeof(value) != "object") + { + return false; + } + else + { + var ret = this.target.releaseNamedASObject(value.fb_instance_id); + return ret; + } + }, +//create a new AS Object + create: function(className) + { + return this.deserialize(this.target.create(className)); + }, + + + // utilities + + makeID: function(token) + { + return (this.bridgeID << 16) + token; + }, + + + // low level access to the flash object + +//get a named property from an AS object + getPropertyFromAS: function(objRef, propName) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.getPropFromAS(objRef, propName); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, +//set a named property on an AS object + setPropertyInAS: function(objRef,propName, value) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.setPropInAS(objRef,propName, this.serialize(value)); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, + +//call an AS function + callASFunction: function(funcID, args) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.invokeASFunction(funcID, this.serialize(args)); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, +//call a method on an AS object + callASMethod: function(objID, funcName, args) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + args = this.serialize(args); + retVal = this.target.invokeASMethod(objID, funcName, args); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, + + // responders to remote calls from flash + + //callback from flash that executes a local JS function + //used mostly when setting js functions as callbacks on events + invokeLocalFunction: function(funcID, args) + { + var result; + var func = this.localFunctionCache[funcID]; + + if(func != undefined) + { + result = this.serialize(func.apply(null, this.deserialize(args))); + } + + return result; + }, + + // Object Types and Proxies + + // accepts an object reference, returns a type object matching the obj reference. + getTypeFromName: function(objTypeName) + { + return this.remoteTypeCache[objTypeName]; + }, + //create an AS proxy for the given object ID and type + createProxy: function(objID, typeName) + { + var objType = this.getTypeFromName(typeName); + instanceFactory.prototype = objType; + var instance = new instanceFactory(objID); + this.remoteInstanceCache[objID] = instance; + return instance; + }, + //return the proxy associated with the given object ID + getProxy: function(objID) + { + return this.remoteInstanceCache[objID]; + }, + + // accepts a type structure, returns a constructed type + addTypeDataToCache: function(typeData) + { + var newType = new ASProxy(this, typeData.name); + var accessors = typeData.accessors; + for (var i = 0; i < accessors.length; i++) + { + this.addPropertyToType(newType, accessors[i]); + } + + var methods = typeData.methods; + for (var i = 0; i < methods.length; i++) + { + if (FABridge.blockedMethods[methods[i]] == undefined) + { + this.addMethodToType(newType, methods[i]); + } + } + + + this.remoteTypeCache[newType.typeName] = newType; + return newType; + }, + + //add a property to a typename; used to define the properties that can be called on an AS proxied object + addPropertyToType: function(ty, propName) + { + var c = propName.charAt(0); + var setterName; + var getterName; + if(c >= "a" && c <= "z") + { + getterName = "get" + c.toUpperCase() + propName.substr(1); + setterName = "set" + c.toUpperCase() + propName.substr(1); + } + else + { + getterName = "get" + propName; + setterName = "set" + propName; + } + ty[setterName] = function(val) + { + this.bridge.setPropertyInAS(this.fb_instance_id, propName, val); + } + ty[getterName] = function() + { + return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); + } + }, + + //add a method to a typename; used to define the methods that can be callefd on an AS proxied object + addMethodToType: function(ty, methodName) + { + ty[methodName] = function() + { + return this.bridge.deserialize(this.bridge.callASMethod(this.fb_instance_id, methodName, FABridge.argsToArray(arguments))); + } + }, + + // Function Proxies + + //returns the AS proxy for the specified function ID + getFunctionProxy: function(funcID) + { + var bridge = this; + if (this.remoteFunctionCache[funcID] == null) + { + this.remoteFunctionCache[funcID] = function() + { + bridge.callASFunction(funcID, FABridge.argsToArray(arguments)); + } + } + return this.remoteFunctionCache[funcID]; + }, + + //reutrns the ID of the given function; if it doesnt exist it is created and added to the local cache + getFunctionID: function(func) + { + if (func.__bridge_id__ == undefined) + { + func.__bridge_id__ = this.makeID(this.nextLocalFuncID++); + this.localFunctionCache[func.__bridge_id__] = func; + } + return func.__bridge_id__; + }, + + // serialization / deserialization + + serialize: function(value) + { + var result = {}; + + var t = typeof(value); + //primitives are kept as such + if (t == "number" || t == "string" || t == "boolean" || t == null || t == undefined) + { + result = value; + } + else if (value instanceof Array) + { + //arrays are serializesd recursively + result = []; + for (var i = 0; i < value.length; i++) + { + result[i] = this.serialize(value[i]); + } + } + else if (t == "function") + { + //js functions are assigned an ID and stored in the local cache + result.type = FABridge.TYPE_JSFUNCTION; + result.value = this.getFunctionID(value); + } + else if (value instanceof ASProxy) + { + result.type = FABridge.TYPE_ASINSTANCE; + result.value = value.fb_instance_id; + } + else + { + result.type = FABridge.TYPE_ANONYMOUS; + result.value = value; + } + + return result; + }, + + //on deserialization we always check the return for the specific error code that is used to marshall NPE's into JS errors + // the unpacking is done by returning the value on each pachet for objects/arrays + deserialize: function(packedValue) + { + + var result; + + var t = typeof(packedValue); + if (t == "number" || t == "string" || t == "boolean" || packedValue == null || packedValue == undefined) + { + result = this.handleError(packedValue); + } + else if (packedValue instanceof Array) + { + result = []; + for (var i = 0; i < packedValue.length; i++) + { + result[i] = this.deserialize(packedValue[i]); + } + } + else if (t == "object") + { + for(var i = 0; i < packedValue.newTypes.length; i++) + { + this.addTypeDataToCache(packedValue.newTypes[i]); + } + for (var aRefID in packedValue.newRefs) + { + this.createProxy(aRefID, packedValue.newRefs[aRefID]); + } + if (packedValue.type == FABridge.TYPE_PRIMITIVE) + { + result = packedValue.value; + } + else if (packedValue.type == FABridge.TYPE_ASFUNCTION) + { + result = this.getFunctionProxy(packedValue.value); + } + else if (packedValue.type == FABridge.TYPE_ASINSTANCE) + { + result = this.getProxy(packedValue.value); + } + else if (packedValue.type == FABridge.TYPE_ANONYMOUS) + { + result = packedValue.value; + } + } + return result; + }, + //increases the reference count for the given object + addRef: function(obj) + { + this.target.incRef(obj.fb_instance_id); + }, + //decrease the reference count for the given object and release it if needed + release:function(obj) + { + this.target.releaseRef(obj.fb_instance_id); + }, + + // check the given value for the components of the hard-coded error code : __FLASHERROR + // used to marshall NPE's into flash + + handleError: function(value) + { + if (typeof(value)=="string" && value.indexOf("__FLASHERROR")==0) + { + var myErrorMessage = value.split("||"); + if(FABridge.refCount > 0 ) + { + FABridge.refCount--; + } + throw new Error(myErrorMessage[1]); + return value; + } + else + { + return value; + } + } +}; + +// The root ASProxy class that facades a flash object + +ASProxy = function(bridge, typeName) +{ + this.bridge = bridge; + this.typeName = typeName; + return this; +}; +//methods available on each ASProxy object +ASProxy.prototype = +{ + get: function(propName) + { + return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); + }, + + set: function(propName, value) + { + this.bridge.setPropertyInAS(this.fb_instance_id, propName, value); + }, + + call: function(funcName, args) + { + this.bridge.callASMethod(this.fb_instance_id, funcName, args); + }, + + addRef: function() { + this.bridge.addRef(this); + }, + + release: function() { + this.bridge.release(this); + } +}; // Copyright: Hiroshi Ichikawa // License: New BSD License