diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0fdf413..0000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "vendor/node-static"] - path = vendor/node-static - url = git://github.com/maccman/node-static.git -[submodule "vendor/redis-client"] - path = vendor/redis-client - url = git://github.com/maccman/redis-node-client.git -[submodule "vendor/socket.io"] - path = vendor/socket.io - url = git://github.com/maccman/Socket.IO-node.git diff --git a/README.md b/README.md index 54b1e11..07e4cbb 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -# Juggernaut +#Juggeranut -Juggernaut lets you push data to browser, which means you can do awesome -things like multiplayer gaming, chat, realtime collaboration and more! +Juggernaut gives you a realtime connection between your servers and client browsers. +You can literally push data to clients using your web application, which lets you do awesome things like multiplayer gaming, chat, group collaboration and more. -Juggernaut is super simple and easy to get going. -Juggernaut 2, which is a completely rewrite, is built on node.js, is insanely fast, and can scale horizontally to millions of clients. +Juggernaut is built on top of [Node.js](http://nodejs.org) and is super simple and easy to get going. -## Features +##Features -* node.js server +* [Node.js](http://nodejs.org) server * Ruby client * Supports the following protocols: * WebSocket @@ -17,80 +16,122 @@ Juggernaut 2, which is a completely rewrite, is built on node.js, is insanely fa * Server-Sent Events (Opera) * XHR with multipart encoding * XHR with long-polling +* Horizontal scaling * Reconnection support * SSL support -## Subscribe (JavaScript) +As you can see, Juggernaut supports a variety of protocols. If one isn't supported by a client, Juggernaut will fallback to one that is. - - +Supported browsers are: -## Publish (Ruby) +* Desktop + * Internet Explorer >= 5.5 + * Safari >= 3 + * Google Chrome >= 4 + * Firefox >= 3 + * Opera 10.61 +* Mobile + * iPhone Safari + * iPad Safari + * Android WebKit + * WebOs WebKit - Juggernaut.publish("channel_name", {:some => "data"}) - Juggernaut.publish(["channel1", "channel2"], "foo") - -## Requirements +##Requirements * Node.js * Redis -* Ruby - -## Setup +* Ruby (optional) + +##Setup ###Install [Node.js](http://nodejs.org) - wget http://nodejs.org/dist/node-v0.2.4.tar.gz - tar -xzvf node-v0.2.4.tar.gz - cd node-v0.2.4 - ./configure - make - sudo make install +If you're using the [Brew](http://mxcl.github.com/homebrew) package management system, use that: + + brew install node + +Or follow the [Node build instructions](https://github.com/joyent/node/wiki/Installation) ###Install [Redis](http://code.google.com/p/redis) - wget http://redis.googlecode.com/files/redis-2.0.3.tar.gz - tar -xzvf redis-2.0.3.tar.gz - cd redis-2.0.3 - make +If you're using the Brew package, use that: + + brew install redis + +Or follow the [Redis build instructions](http://redis.io/download) + +###Install Juggernaut -###Install the [Juggernaut](http://rubygems.org/gems/juggernaut) gem (optional) +Juggernaut is distributed by [npm](http://npmjs.org), you'll need to [install that](http://npmjs.org) first if you haven't already. + + npm install juggernaut + +###Install the [Juggernaut client gem](http://rubygems.org/gems/juggernaut) + +This step is optional, but if you're planning on using Juggernaut with Ruby, you'll need the gem. gem install juggernaut ##Running -Start Redis - - cd redis-2.0.3 - ./redis-server redis.conf +Start Redis: + + redis-server -Download Juggernaut, and start the Juggernaut server: +Start Juggernaut: - git clone git://github.com/maccman/juggernaut.git --recursive - cd juggernaut - node server.js + juggernaut That's it! Now go to [http://localhost:8080](http://localhost:8080) to see Juggernaut in action. -## Flash +##Basic usage + +Everything in Juggernaut is done within the context of a channel. JavaScript clients can subscribe to a channel which your server can publish to. +First, we need to include Juggernaut's application.js file. By default, Juggernaut is hosted on port 8080 - so we can just link to the file there. + + + +We then need to instantiate the Juggernaut object and subscribe to the channel. As you can see, subscribe takes two arguments, the channel name and a callback. + + + +That's it for the client side. Now, to publish to the channel we'll write some Ruby: + + require "juggernaut" + Juggernaut.publish("channel1", "Some data") + +You should see the data we sent appear instantly in the [open browser window](http://localhost:8080). +As well as strings, we can even pass objects, like so: + + Juggernaut.publish("channel1", {:some => "data"}) + +The publish method also takes an array of channels, in case you want to send a message to multiple channels co-currently. -Flash is optional, but it's the default fallback for Firefox (until the beta is released). -Start the server using root if you want Flash support. It needs to open a restricted port. -Also, you need to specify the location of WebSocketMain.swf + Juggernaut.publish(["channel1", "channel2"], ["foo", "bar"]) -Either copy it to your web app root, or set the address like this: +That's pretty much the gist of it, the two methods - publish and subscribe. Couldn't be easier than that! + +##Flash + +Adobe Flash is optional, but it's the default fallback for a lot of browsers until WebSockets are supported. +However, Flash needs a XML policy file to be served from port 843, which is restricted. You'll need to run Juggernaut with root privileges in order to open that port. + + sudo juggernaut + +You'll also need to specify the location of WebSocketMain.swf. Either copy this file (from Juggernaut's public directory) to the root public directory of your application, or specify it's location before instantiating Juggernaut: window.WEB_SOCKET_SWF_LOCATION = "http://juggaddress:8080/WebSocketMain.swf" - -## SSL + +As I mentioned above, using Flash with Juggernaut is optional - you don't have to run the server with root privileges. If Flash isn't available, Juggernaut will use [WebSockets](http://en.wikipedia.org/wiki/WebSocket) (the default), [Comet](http://goo.gl/lO6S) or polling. + +##SSL -Juggernaut has SSL support! To activate, just put create a folder called 'keys' in the 'juggernaut' dir, +Juggernaut has SSL support! To activate, just put create a folder called 'keys' in the 'juggernaut' directory, containing your privatekey.pem and certificate.pem files. >> mkdir keys @@ -99,53 +140,258 @@ containing your privatekey.pem and certificate.pem files. >> openssl req -new -key privatekey.pem -out certrequest.csr >> openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem -Then, pass the secure option to Juggernaut: - +Then, pass the secure option when instantiating Juggernaut in JavaScript: + var juggernaut = new Juggernaut({secure: true}) + +All Juggernaut's communication will now be encrypted by SSL. -## Daemonize +##Scaling -[http://kevin.vanzonneveld.net/techblog/article/run_nodejs_as_a_service_on_ubuntu_karmic](http://kevin.vanzonneveld.net/techblog/article/run_nodejs_as_a_service_on_ubuntu_karmic) +The only centralised (i.e. potential bottle neck) part to Juggernaut is Redis. +Redis can support hundreds of thousands writes a second, so it's unlikely that will be an issue. -# Scaling +Scaling is just a case of starting up more Juggernaut Node servers, all sharing the same Redis instance. +Put a TCP load balancer in front them, distribute clients with a Round Robin approach, and use sticky sessions. -Just create more Juggernaut daemons. Put a TCP load balancer in front of them. -Make sure they all connect to the same Redis instance. Use sticky sessions. +It's worth noting that the latest WebSocket specification breaks support for a lot of HTTP load balancers, so it's safer just using a TCP one. -## Usage case - Group Chat +##Client Events - - +Juggernaut's JavaScript client has a few events that you can bind to: - Juggernaut.publish("/chats", params[:body]) +* connect +* disconnect +* reconnect -## Usage case - Private Chat +Juggernaut also triggers data events in the context of an channel. You can bind to that event by just passing a callback to the subscribe function. +Here's an example of event binding. We're using [jQuery UI](http://jqueryui.com) to show a popup when the client loses their connection to our server. - - + var jug = new Juggernaut; + + var offline = $("
") + .html("The connection has been disconnected!
" + + "Please go back online to use this service.") + .dialog({ + autoOpen: false, + modal: true, + width: 330, + resizable: false, + closeOnEscape: false, + title: "Connection" + }); + + jug.on("connect", function(){ + offline.dialog("close"); + }); + + jug.on("disconnect", function(){ + offline.dialog("open"); + }); + + // Once we call subscribe, Juggernaut tries to connnect. + jug.subscribe("channel1", function(data){ + console.log("Got data: " + data); + }); - Juggernaut.publish(users.map {|u| "/chats/#{u.id}" }, params[:body]) +##Excluding certain clients -## Usage case - Model Synchronisation +It's a common use case to send messages to every client, except one. For example, this is a common chat scenario: -### Implement sync_clients on models +* User creates chat message +* User's client appends the message to the chat log, so the user sees it instantly +* User's client sends an AJAX request to the server, notifying it of the new chat message +* The server then publishes the chat message to all relevant clients - def sync_clients - users.map(&:id) +Now, the issue above is if the server publishes the chat message back to the original client. In which case, it would get duplicated in the chat logs (as it already been created). We can resolve this issue by recording the client's Juggernaut ID, and then passing it as an `:except` option when Juggernaut publishes. + +You can pass the Juggernaut session ID along with any AJAX requests by hooking into `beforeSend`, which is triggered by jQuery before sending any AJAX requests. The callback is passed an XMLHttpRequest, which we can use to set a custom header specifying the session ID. + + var jug = new Juggernaut; + + jQuery.beforeSend(function(xhr){ + xhr.setRequestHeader("X-Session-ID", jug.sessionID); + }); + +Now, when we publish to a channel, we can pass the `:except` option, with the current client's session ID. + + Juggernaut.publish( + "/chat", + params[:body], + :except => request.headers["X-Session-ID"] + ) + +Now, the original client won't get the duplicated chat message, even if it's subscribed to the __/chat__ channel. + +##Server Events + +When a client connects & disconnects, Juggernaut triggers a callback. You can listen to these callbacks from the Ruby client, + + Juggernaut.subscribe do |event, data| + # Use event/data + end + +The event is either `:subscribe` or `:unsubscribe`. The data variable is just a hash of the client details: + + {"channel" => "channel1", "session_id" => "1822913980577141", "meta" => "foo"} + +##Metadata + +You'll notice there's a meta attribute in the server event example above. Juggernaut lets you attach meta data to the client object, +which gets passed along to any server events. For example, you could set User ID meta data - then you would know which user was subscribing/unsubscribing to channels. You could use this information to build a live Roster of online users. + + var jug = new Juggernaut; + jug.meta = {user_id: 1}; + +##Using Juggernaut from Python + +You don't have to use Ruby to communicate with Juggernaut. In fact, all that is needed is a [Redis](http://code.google.com/p/redis) adapter. Here we're using [Python](http://www.python.org) with [redis-py](http://github.com/andymccurdy/redis-py). + + import redis + import json + + msg = { + "channels": ["channel1"], + "data": "foo" + } + + r = redis.Redis() + r.publish("juggernaut", json.dumps(msg)) + +##Using Juggernaut from Node.js + +Similar to the Python example, we can use a Node.js Redis adapter to publish to Juggernaut. + + var redis = require("redis"); + + var msg = { + "channels": ["channel1"], + "data": "foo" + }; + + var client = redis.createClient(); + client.publish("juggernaut", JSON.stringify(msg)); + +##Building a Roster + +So, let's take all we've learnt about Juggernaut, and apply it to something practical - a live chat roster. +Here's the basic class. We're using [SuperModel](http://github.com/maccman/supermodel) with the Redis adapter. Any changes to the model will be saved to our Redis data store. We're also associating each Roster record with a user. + + class Roster < SuperModel::Base + include SuperModel::Redis::Model + include SuperModel::Timestamp::Model + + belongs_to :user + validates_presence_of :user_id + + indexes :user_id + end + +Now let's integrate the Roster class with Juggernaut. We're going to listen to Juggernaut's server events - fetching the user_id out of the events meta data, and calling __event_subscribe__ or __event_unsubscribe__, depending on the event type. + + def self.subscribe + Juggernaut.subscribe do |event, data| + user_id = data["meta"] && data["meta"]["user_id"] + next unless user_id + + case event + when :subscribe + event_subscribe(user_id) + when :unsubscribe + event_unsubscribe(user_id) + end + end + end + +Let's implement those two methods __event_subscribe__ & __event_unsubscribe__. We need to take into account they may be called multiple times for a particular user_id, if a User opens multiple browser windows co-currently. + + def event_subscribe(user_id) + record = find_by_user_id(user_id) || self.new(:user_id => user_id) + record.increment! + end + + def event_unsubscribe(user_id) + record = find_by_user_id(user_id) + record && record.decrement! + end + +We need to add a __count__ attribute to the Roster class, so we can track if a client has completely disconnected from the system. +Whenever clients subscribes to a channel, __increment!__ will get called and the __count__ attribute will be incremented, conversly whenever they disconnect from that channel __decrement!__ will get called and __count__ decremented. + + attributes :count + + def count + read_attribute(:count) || 0 end + + def increment! + self.count += 1 + save! + end + + def decrement! + self.count -= 1 + self.count > 0 ? save! : destroy + end + +When __decrement!__ is called, we check to see if the count is zero, i.e. a client is no longer connected, and destroy the record if necessary. Now, at this point we have a live list of Roster records indicating who's online. We just need to call __Roster.subscribe__, say in a Rails script file, and Juggernaut events will be processed. + + #!/usr/bin/env ruby + require File.expand_path('../../config/environment', __FILE__) + + puts "Starting Roster" + Roster.subscribe + +There's no point, however, in having a live Roster unless we can show that to users - which is the subject of the next section, observing models. + +##Observing models + +We can create an Juggernaut observer, which will observe some of the models, notifying clients when they're changed. + + class JuggernautObserver < ActiveModel::Observer + observe :roster + + def after_create(rec) + publish(:create, rec) + end + + def after_update(rec) + publish(:update, rec) + end -Check out client/examples/juggernaut_observer.rb and client/examples/juggernaut_observer.js + def after_destroy(rec) + publish(:destroy, rec) + end + + protected + def publish(type, rec) + channels = Array(rec.observer_clients).map {|c| "/observer/#{c}" } + Juggernaut.publish( + channels, + { + :id => rec.id, + :type => type, + :klass => rec.class.name, + :record => rec + } + ) + end + end + +So, you can see we're calling the publish method whenever a record is created/updated/destroyed. You'll notice that we're calling __observer_clients__ on the updated record. This is a method that application specific, and needs to be implemented on the Roster class. It needs to return an array of user_ids associated with the record. + +So, as to the JavaScript side to the observer, we need to subscribe to a observer channel and set a callback. Now, whenever a __Roster__ record is created/destroyed, the process function will be called. We can then update the UI accordingly. + + var process = function(msg){ + // msg.klass + // msg.type + // msg.id + // msg.record + }; + + var jug = new Juggernaut; + jug.subscribe("/observer/" + user_id, process); + +##Full examples + +You can see the full examples inside [Holla](http://github.com/maccman/holla), specifically [roster.rb](http://github.com/maccman/holla/blob/master/app/models/roster.rb), [juggernaut_observer.rb](http://github.com/maccman/holla/blob/master/app/observers/juggernaut_observer.rb) and [application.juggernaut.js](http://github.com/maccman/holla/blob/master/app/javascripts/application.juggernaut.js). diff --git a/index.js b/index.js index 08bdffa..57e91a7 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,2 @@ -require("fs").readdirSync("./vendor").forEach(function(name){ - require.paths.unshift("./vendor/" + name + "/lib"); -}); - require.paths.unshift("./lib"); -module.exports = require("./lib/juggernaut"); - +module.exports = require("./lib/juggernaut"); \ No newline at end of file diff --git a/lib/juggernaut/channel.js b/lib/juggernaut/channel.js index 5ec3f15..ec743ca 100644 --- a/lib/juggernaut/channel.js +++ b/lib/juggernaut/channel.js @@ -1,16 +1,14 @@ -var sys = require("sys"); - +var sys = require("sys"); var Events = require("./events"); -var SuperClass = require("superclass"); -Channel = module.exports = new SuperClass; +Channel = module.exports = require("./klass").create(); Channel.extend({ channels: {}, find: function(name){ if ( !this.channels[name] ) - this.channels[name] = new Channel(name) + this.channels[name] = Channel.inst(name) return this.channels[name]; }, @@ -55,4 +53,4 @@ Channel.include({ this.clients = this.clients.delete(client); Events.unsubscribe(this, client); } -}); +}); \ No newline at end of file diff --git a/lib/juggernaut/client.js b/lib/juggernaut/client.js index b62b4fa..a377f6f 100644 --- a/lib/juggernaut/client.js +++ b/lib/juggernaut/client.js @@ -1,10 +1,8 @@ -var sys = require("sys"); -var SuperClass = require("superclass"); -var Channel = require("./channel"); -var JUtils = require("jutils"); -var Events = require("./events"); +var sys = require("sys"); +var Channel = require("./channel"); +var Events = require("./events"); -Client = module.exports = new SuperClass; +Client = module.exports = require("./klass").create(); Client.include({ init: function(conn){ @@ -36,8 +34,8 @@ Client.include({ write: function(message){ if (message.except) { - except = JUtils.makeArray(message.except) - if (except.indexOf(this.session_id) != -1) + except = Array.makeArray(message.except); + if (except.include(this.session_id)) return false; } diff --git a/lib/juggernaut/connection.js b/lib/juggernaut/connection.js index 417558c..eced9cd 100644 --- a/lib/juggernaut/connection.js +++ b/lib/juggernaut/connection.js @@ -1,13 +1,12 @@ -var sys = require("sys"); -var SuperClass = require("superclass"); -var Client = require("./client"); -Connection = module.exports = new SuperClass; +var sys = require("sys"); +var Client = require("./client"); +Connection = module.exports = require("./klass").create(); Connection.include({ init: function(stream){ this.stream = stream; this.session_id = this.stream.sessionId; - this.client = new Client(this); + this.client = Client.inst(this); this.stream.on("message", this.proxy(this.onmessage)); this.stream.on("disconnect", this.proxy(this.ondisconnect)); diff --git a/lib/juggernaut/ext/array.js b/lib/juggernaut/ext/array.js index fc16e4e..b3176f0 100644 --- a/lib/juggernaut/ext/array.js +++ b/lib/juggernaut/ext/array.js @@ -1,3 +1,9 @@ +Array.makeArray = function(value){ + if ( !value ) return []; + if ( typeof value == "string" ) return [value]; + return Array.prototype.slice.call(value, 0); +}; + Array.prototype.include = function(value){ return(this.indexOf(value) != -1); }; diff --git a/lib/juggernaut/index.js b/lib/juggernaut/index.js index 644fdb9..58f0c55 100644 --- a/lib/juggernaut/index.js +++ b/lib/juggernaut/index.js @@ -5,6 +5,6 @@ var Server = require("./server"); module.exports.listen = function(port){ Publish.listen(); - var server = new Server; + var server = Server.inst(); server.listen(port); }; \ No newline at end of file diff --git a/lib/juggernaut/klass.js b/lib/juggernaut/klass.js new file mode 100644 index 0000000..5a9ef29 --- /dev/null +++ b/lib/juggernaut/klass.js @@ -0,0 +1,71 @@ +var Klass = module.exports = { + initializer: function(){}, + init: function(){}, + + prototype: { + initializer: function(){}, + init: function(){} + }, + + create: function(include, extend){ + var object = Object.create(this); + object.parent = this; + object.prototype = object.fn = Object.create(this.prototype); + + if (include) object.include(include); + if (extend) object.extend(extend); + + object.initializer.apply(object, arguments); + object.init.apply(object, arguments); + return object; + }, + + inst: function(){ + var instance = Object.create(this.prototype); + instance.parent = this; + + instance.initializer.apply(instance, arguments); + instance.init.apply(instance, arguments); + return instance; + }, + + proxy: function(func){ + var thisObject = this; + return(function(){ + return func.apply(thisObject, arguments); + }); + }, + + proxyAll: function(){ + var functions = makeArray(arguments); + for (var i=0; i < functions.length; i++) + this[functions[i]] = this.proxy(this[functions[i]]); + }, + + include: function(obj){ + var included = obj.included || obj.setup; + + delete obj.included; + delete obj.extended; + delete obj.setup; + + for(var i in obj) + this.fn[i] = obj[i]; + if (included) included.apply(this); + }, + + extend: function(obj){ + var extended = obj.extended || obj.setup; + + delete obj.included; + delete obj.extended; + delete obj.setup; + + for(var i in obj) + this[i] = obj[i]; + if (extended) extended.apply(this); + } +}; + +Klass.prototype.proxy = Klass.proxy; +Klass.prototype.proxyAll = Klass.proxyAll; \ No newline at end of file diff --git a/lib/juggernaut/message.js b/lib/juggernaut/message.js index 6794dda..a462545 100644 --- a/lib/juggernaut/message.js +++ b/lib/juggernaut/message.js @@ -1,5 +1,3 @@ -var JUtils = require("jutils"); - Message = module.exports = function(hash){ for (var key in hash) this[key] = hash[key]; }; @@ -18,7 +16,7 @@ Message.prototype.toJSON = function(){ }; Message.prototype.getChannels = function(){ - return(JUtils.makeArray(this.channels || this.channel)); + return(Array.makeArray(this.channels || this.channel)); }; Message.prototype.getChannel = function(){ diff --git a/lib/juggernaut/publish.js b/lib/juggernaut/publish.js index 862f722..0f426b4 100644 --- a/lib/juggernaut/publish.js +++ b/lib/juggernaut/publish.js @@ -6,7 +6,8 @@ var Channel = require("./channel"); Publish = module.exports = {}; Publish.listen = function(){ this.client = redis.createClient(); - this.client.subscribeTo("juggernaut", function(_, data) { + + this.client.on("message", function(_, data) { sys.log("Received: " + data); try { @@ -15,4 +16,6 @@ Publish.listen = function(){ Channel.publish(message); }); -}; \ No newline at end of file + + this.client.subscribe("juggernaut"); +}; diff --git a/lib/juggernaut/redis.js b/lib/juggernaut/redis.js index 86bc719..7974cda 100644 --- a/lib/juggernaut/redis.js +++ b/lib/juggernaut/redis.js @@ -1,13 +1,23 @@ -var url = require("url"); -var redis = require("redis-client"); +var sys = require("sys"); +var url = require("url"); +var redis = require("redis"); // redis.DEFAULT_PORT = 6380; module.exports.createClient = function(){ + var client; + if (process.env.REDISTOGO_URL) { var address = url.parse(process.env.REDISTOGO_URL); - return redis.createClient(address.port, address.hostname); + client = redis.createClient(address.port, address.hostname); + } else { + client = redis.createClient(); } - return redis.createClient(); + // Prevent redis calling exit + client.on("error", function(error){ + sys.error(error); + }); + + return client; }; \ No newline at end of file diff --git a/lib/juggernaut/server.js b/lib/juggernaut/server.js index 2b5546b..a4864fe 100644 --- a/lib/juggernaut/server.js +++ b/lib/juggernaut/server.js @@ -1,16 +1,12 @@ var http = require("http"); var https = require("https"); var sys = require("sys"); - +var path = require("path"); +var fs = require("fs"); var io = require("socket.io"); -var nstatic = require("node-static"); - -var SuperClass = require("superclass"); +var nstatic = require("node-static-maccman"); var Connection = require("./connection"); -var path = require("path"); -var fs = require("fs"); - var credentials; if (path.existsSync("keys/privatekey.pem")) { var privateKey = fs.readFileSync("keys/privatekey.pem", "utf8"); @@ -18,9 +14,9 @@ if (path.existsSync("keys/privatekey.pem")) { credentials = {key: privateKey, cert: certificate}; } -Server = module.exports = new SuperClass; +Server = module.exports = require("./klass").create(); -var fileServer = new nstatic.Server("./public"); +var fileServer = new nstatic.Server(path.normalize(__dirname + "../../../public")); Server.include({ init: function(){ @@ -30,7 +26,7 @@ Server.include({ fileServer.serve(request, response, function (err, res) { if (err) { // An error as occured sys.error("> Error serving " + request.url + " - " + err.message); - response.writeHead(err.status, err.headers); + response.writeHead(err.status || 500, err.headers); response.end(); } else { // The file was served successfully sys.log("Serving " + request.url + " - " + res.message); @@ -47,11 +43,11 @@ Server.include({ } this.socket = io.listen(this.httpServer); - this.socket.on("connection", function(stream){ new Connection(stream) }); + this.socket.on("connection", function(stream){ Connection.inst(stream) }); }, listen: function(port){ port = parseInt(port || process.env.PORT || 8080); this.httpServer.listen(port); } -}); +}); \ No newline at end of file diff --git a/lib/jutils.js b/lib/jutils.js deleted file mode 100644 index 415c002..0000000 --- a/lib/jutils.js +++ /dev/null @@ -1,151 +0,0 @@ -// Helper functions, mostly taken from jQuery -// MIT License - http://jquery.org/license - -JUtils = module.exports = {}; - -JUtils.isFunction = function( obj ) { - return toString.call(obj) === "[object Function]"; -}; - -JUtils.isArray = function( obj ) { - return toString.call(obj) === "[object Array]"; -}; - -JUtils.isPlainObject = function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) { - return false; - } - - // Not own constructor property must be Object - if ( obj.constructor - && !hasOwnProperty.call(obj, "constructor") - && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || hasOwnProperty.call( obj, key ); -}; - -JUtils.export = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !JUtils.isFunction(target) ) { - target = {}; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging object literal values or arrays - if ( deep && copy && ( JUtils.isPlainObject(copy) || JUtils.isArray(copy) ) ) { - var clone = src && ( JUtils.isPlainObject(src) || JUtils.isArray(src) ) ? src - : JUtils.isArray(copy) ? [] : {}; - - // Never move original objects, clone them - target[ name ] = JUtils.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -JUtils.merge = function( first, second ) { - var i = first.length, j = 0; - - if ( typeof second.length === "number" ) { - for ( var l = second.length; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; -}; - -JUtils.grep = function( elems, callback, inv ) { - var ret = []; - - // Go through the array, only saving the items - // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) { - if ( !inv !== !callback( elems[ i ], i ) ) { - ret.push( elems[ i ] ); - } - } - - return ret; -}, - -// arg is for internal usage only -JUtils.map = function( elems, callback, arg ) { - var ret = [], value; - - // Go through the array, translating each of the items to their - // new value (or values). - for ( var i = 0, length = elems.length; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - return ret.concat.apply( [], ret ); -}; - -var push = Array.prototype.push; - -JUtils.makeArray = function(array, results){ - var ret = results || []; - - if ( array != null ) { - if ( array.length == null || typeof array === "string" || JUtils.isFunction(array) ) { - push.call( ret, array ); - } else { - JUtils.merge( ret, array ); - } - } - - return ret; -}; \ No newline at end of file diff --git a/lib/superclass.js b/lib/superclass.js deleted file mode 100644 index 07e7949..0000000 --- a/lib/superclass.js +++ /dev/null @@ -1,57 +0,0 @@ -var JUtils = require("jutils"); - -SuperClass = module.exports = function(parent){ - var result = function(){ - this.init.apply(this, arguments); - }; - - result.prototype.init = function(){}; - - if (parent){ - for(var i in parent){ - result[i] = SuperClass.clone(parent[i]); - } - for(var i in parent.prototype){ - result.prototype[i] = SuperClass.clone(parent.prototype[i]); - } - result._super = parent; - result.prototype._super = parent.prototype; - } - - result.fn = result.prototype; - - result.extend = function(obj){ - var extended = obj.extended; - for(var i in obj){ - result[i] = obj[i]; - } - if (extended) extended(result) - }; - - result.include = function(obj){ - var included = obj.included; - for(var i in obj){ - result.fn[i] = obj[i]; - } - if (included) included(result) - }; - - result.proxy = function(func){ - var thisObject = this; - return(function(){ - return func.apply(thisObject, arguments); - }); - } - result.fn.proxy = result.proxy; - - result.fn._class = result; - - return result; -}; - -SuperClass.clone = function(obj){ - if (typeof obj == "function") return obj; - if (typeof obj != "object") return obj; - if (JUtils.isArray(obj)) return JUtils.extend([], obj); - return JUtils.extend({}, obj); -}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..eafbc32 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ "name" : "juggernaut" +, "description" : "Realtime PubSub server push." +, "version" : "2.0.4" +, "author" : "maccman" +, "licenses" : + [ { "type" : "MIT" + , "url" : "https://github.com/maccman/juggernaut/blob/master/LICENSE" + } + ] +, "repository" : + { "type" : "git" + , "url" : "http://github.com/maccman/juggernaut.git" + } +, "engine" : [ "node >=0.1.102" ] +, "main" : "./index" +, "bin" : { "juggernaut" : "./server.js" } +, "dependencies" : + { "socket.io" : "~0.6.16" + , "redis" : "~0.5.9" + , "node-static-maccman" : "~0.5.3" + } +} \ No newline at end of file diff --git a/public/WebSocketMain.swf b/public/WebSocketMain.swf index 0bec17c..694f9dc 100644 Binary files a/public/WebSocketMain.swf and b/public/WebSocketMain.swf differ diff --git a/public/application.js b/public/application.js index cb0332b..fdc9b89 100644 --- a/public/application.js +++ b/public/application.js @@ -1 +1 @@ -if(!this.JSON){JSON=function(){function f(n){return n<10?"0"+n:n}Date.prototype.toJSON=function(){return this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z"};var m={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v;switch(typeof value){case"string":return r.test(value)?'"'+value.replace(r,function(a){var c=m[a];if(c){return c}c=a.charCodeAt();return"\\u00"+Math.floor(c/16).toString(16)+(c%16).toString(16)})+'"':'"'+value+'"';case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}if(typeof value.toJSON==="function"){return stringify(value.toJSON())}a=[];if(typeof value.length==="number"&&!(value.propertyIsEnumerable("length"))){l=value.length;for(i=0;i");this._doc.parentWindow.s=this;this._doc.close();var a=this._doc.createElement("div");this._doc.body.appendChild(a);this._iframe=this._doc.createElement("iframe");a.appendChild(this._iframe);this._iframe.src=this._prepareUrl()+"/"+(+new Date)};b.prototype._=function(f,e){this._onData(f);var a=e.getElementsByTagName("script")[0];a.parentNode.removeChild(a)};b.prototype._destroy=function(){this._iframe.src="about:blank";this._doc=null;CollectGarbage()};b.prototype.disconnect=function(){this._destroy();return io.Transport.XHR.prototype.disconnect.call(this)};b.check=function(){if("ActiveXObject" in window){try{var a=new ActiveXObject("htmlfile");return a&&io.Transport.XHR.check()}catch(d){}}return false};b.xdomainCheck=function(){return false}})();(function(){var b=io.Transport["xhr-multipart"]=function(){io.Transport.XHR.apply(this,arguments)};io.util.inherit(b,io.Transport.XHR);b.prototype.type="xhr-multipart";b.prototype._get=function(){var a=this;this._xhr=this._request("","GET",true);this._xhr.onreadystatechange=function(){if(a._xhr.readyState==3){a._onData(a._xhr.responseText)}};this._xhr.send()};b.check=function(){return"XMLHttpRequest" in window&&"prototype" in XMLHttpRequest&&"multipart" in XMLHttpRequest.prototype};b.xdomainCheck=function(){return true}})();(function(){var d=new Function(),c=io.Transport["xhr-polling"]=function(){io.Transport.XHR.apply(this,arguments)};io.util.inherit(c,io.Transport.XHR);c.prototype.type="xhr-polling";c.prototype.connect=function(){if(io.util.ios||io.util.android){var a=this;io.util.load(function(){setTimeout(function(){io.Transport.XHR.prototype.connect.call(a)},10)})}else{io.Transport.XHR.prototype.connect.call(this)}};c.prototype._get=function(){var a=this;this._xhr=this._request(+new Date,"GET");if("onload" in this._xhr){this._xhr.onload=function(){a._onData(this.responseText);a._get()}}else{this._xhr.onreadystatechange=function(){var e;if(a._xhr.readyState==4){a._xhr.onreadystatechange=d;try{e=a._xhr.status}catch(b){}if(e==200){a._onData(a._xhr.responseText);a._get()}else{a._onDisconnect()}}}}this._xhr.send()};c.check=function(){return io.Transport.XHR.check()};c.xdomainCheck=function(){return io.Transport.XHR.xdomainCheck()}})();io.JSONP=[];JSONPPolling=io.Transport["jsonp-polling"]=function(){io.Transport.XHR.apply(this,arguments);this._insertAt=document.getElementsByTagName("script")[0];this._index=io.JSONP.length;io.JSONP.push(this)};io.util.inherit(JSONPPolling,io.Transport["xhr-polling"]);JSONPPolling.prototype.type="jsonp-polling";JSONPPolling.prototype._send=function(l){var e=this;if(!("_form" in this)){var q=document.createElement("FORM"),p=document.createElement("TEXTAREA"),r=this._iframeId="socket_io_iframe_"+this._index,m;q.style.position="absolute";q.style.top="-1000px";q.style.left="-1000px";q.target=r;q.method="POST";q.action=this._prepareUrl()+"/"+(+new Date)+"/"+this._index;p.name="data";q.appendChild(p);this._insertAt.parentNode.insertBefore(q,this._insertAt);document.body.appendChild(q);this._form=q;this._area=p}function o(){n();e._posting=false;e._checkSend()}function n(){if(e._iframe){e._form.removeChild(e._iframe)}try{m=document.createElement('