rcomet 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,188 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'rubygems'
4
+ require 'json'
5
+ require 'rcomet'
6
+ require 'rcomet/channel'
7
+
8
+ module RComet
9
+ class Client
10
+ def initialize( uri_or_string )
11
+ @uri = uri_or_string
12
+ @uri = URI.parse(@uri) if @uri.class == String
13
+ @clientId = nil
14
+ # @interval = nil
15
+ @connection = nil
16
+ @subscriptions = {}
17
+ end
18
+
19
+ # Request Response
20
+ # MUST include: * channel MUST include: * channel
21
+ # * clientId * successful
22
+ # * connectionType * clientId
23
+ # MAY include: * ext MAY include: * error
24
+ # * id * advice
25
+ # * ext
26
+ # * id
27
+ # * timestamp
28
+ def connect
29
+ @connection.kill unless @connection.nil?
30
+ @connection = Thread.new {
31
+ faild = false
32
+ while true
33
+ id = RComet.random(32)
34
+ message = {
35
+ "channel" => RComet::Channel::CONNECT,
36
+ "clientId" => @clientId,
37
+ "connectionType" => "long-polling",
38
+ "id" => id
39
+ }
40
+ r = send( message )
41
+
42
+ if r[0]["id"] == id and r[0]["successful"] == true
43
+ @subscriptions[r[1]["channel"]].call( r[1]["data"])
44
+ elsif r[0]["successful"] == false
45
+ faild = true
46
+ break
47
+ end
48
+ end
49
+
50
+ if faild
51
+ handshake()
52
+ connect()
53
+ end
54
+ }
55
+ end
56
+
57
+ # Request Response
58
+ # MUST include: * channel MUST include: * channel
59
+ # * clientId * successful
60
+ # MAY include: * ext * clientId
61
+ # * id MAY include: * error
62
+ # * ext
63
+ # * id
64
+ def disconnect
65
+ unless @connection.nil?
66
+ @connection.kill
67
+ message = {
68
+ "channel" => RComet::Channel::DISCONNECT,
69
+ "clientId" => @clientId,
70
+ "id" => RComet.random(32)
71
+ }
72
+ r = send( message )
73
+ ## TODO : Check response
74
+ end
75
+ end
76
+
77
+ # Request
78
+ # MUST include: * channel
79
+ # * version
80
+ # * supportedConnectionTypes
81
+ # MAY include: * minimumVersion
82
+ # * ext
83
+ # * id
84
+ #
85
+ # Success Response Failed Response
86
+ # MUST include: * channel MUST include: * channel
87
+ # * version * successful
88
+ # * supportedConnectionTypes * error
89
+ # * clientId MAY include: * supportedConnectionTypes
90
+ # * successful * advice
91
+ # MAY include: * minimumVersion * version
92
+ # * advice * minimumVersion
93
+ # * ext * ext
94
+ # * id * id
95
+ # * authSuccessful
96
+ def handshake
97
+ id = RComet.random(32)
98
+ message = {
99
+ "channel" => RComet::Channel::HANDSHAKE,
100
+ "version" => RComet::BAYEUX_VERSION,
101
+ "supportedConnectionTypes" => [ "long-polling", "callback-polling" ],
102
+ "id" => id
103
+ }
104
+
105
+ response = send( message )[0]
106
+ if response["successful"] and response["id"] == id
107
+ @clientId = response["clientId"]
108
+ # @interval = response["advice"]["interval"]
109
+ else
110
+ raise
111
+ end
112
+ end
113
+
114
+ # Request Response
115
+ # MUST include: * channel MUST include: * channel
116
+ # * data * successful
117
+ # MAY include: * clientId MAY include: * id
118
+ # * id * error
119
+ # * ext * ext
120
+ def publish( channel, data )
121
+ message = {
122
+ "channel" => channel,
123
+ "data" => data,
124
+ "clientId" => @clientId,
125
+ "id" => RComet.random(32)
126
+ }
127
+ r = send(message)[0]
128
+ ## TODO : Check response
129
+ end
130
+
131
+ # Request Response
132
+ # MUST include: * channel MUST include: * channel
133
+ # * clientId * successful
134
+ # * subscription * clientId
135
+ # MAY include: * ext * subscription
136
+ # * id MAY include: * error
137
+ # * advice
138
+ # * ext
139
+ # * id
140
+ # * timestamp
141
+ def subscribe( channel, &block )
142
+ @subscriptions[channel] = block if block_given?
143
+
144
+ message = {
145
+ "channel" => RComet::Channel::SUBSCRIBE,
146
+ "clientId" => @clientId,
147
+ "subscription" => channel,
148
+ "id" => RComet.random(32)
149
+ }
150
+
151
+ r = send(message)
152
+ ## TODO : Check response
153
+ end
154
+
155
+ # Request Response
156
+ # MUST include: * channel MUST include: * channel
157
+ # * clientId * successful
158
+ # * subscription * clientId
159
+ # MAY include: * ext * subscription
160
+ # * id MAY include: * error
161
+ # * advice
162
+ # * ext
163
+ # * id
164
+ # * timestamp
165
+ def unsubscribe( channels )
166
+ channels = [channels] unless channels.class == Array
167
+ channels.each do |c|
168
+ @subscriptions.delete(c)
169
+ end
170
+ message = {
171
+ "channel" => RComet::Channel::UNSUBSCRIBE,
172
+ "clientId" => @clientId,
173
+ "subscription" => channels,
174
+ "id" => RComet.random(32)
175
+ }
176
+
177
+ r = send(message)
178
+ ## TODO : Check response
179
+ end
180
+
181
+ private
182
+ def send( message )
183
+ res = Net::HTTP.post_form( @uri, { "message" => [message].to_json } )
184
+ return JSON.parse( res.body )
185
+ end
186
+
187
+ end
188
+ end
@@ -0,0 +1,5 @@
1
+ class Hash #:nodoc:
2
+ def <<( h )
3
+ self.merge!( h )
4
+ end
5
+ end
@@ -0,0 +1,289 @@
1
+ # The Rack::Adapter class allow you to use RComet as a Rack middleware
2
+ #
3
+ # Example :
4
+ #
5
+ # map '/comet' do
6
+ # run RComet::RackAdapter :mount => "/comet" do
7
+ # # ...
8
+ # end
9
+ # end
10
+ #
11
+ require 'rack'
12
+ require 'json'
13
+
14
+ require 'rcomet'
15
+ require 'rcomet/core_ext'
16
+ require 'rcomet/server'
17
+ require 'rcomet/channel'
18
+ require 'rcomet/user'
19
+
20
+ module RComet
21
+ class RackAdapter #:nodoc:
22
+ def initialize(app = nil, options = nil, &block)
23
+ @app = app if app.respond_to?(:call)
24
+ @options = [app, options].grep(Hash).first
25
+ @channels = RComet::ChannelSet.new
26
+ @users = Hash.new
27
+ @timeout = nil
28
+
29
+ instance_eval(&block) if block
30
+ return @app
31
+ end
32
+
33
+ def call(env)
34
+ request = Rack::Request.new(env)
35
+
36
+ if request.params.empty?
37
+ [404, {'Content-Type' => 'text/html'}, ""]
38
+ else
39
+ messages = JSON.parse(request.params['message'])
40
+ jsonp = request.params['jsonp'] || JSONP_CALLBACK
41
+ get = request.get?
42
+
43
+ process( jsonp, messages, get )
44
+ end
45
+ end
46
+
47
+ def channel
48
+ @channels
49
+ end
50
+
51
+ def timeout( &block )
52
+ @handler = block if block_given?
53
+ @handler
54
+ end
55
+
56
+ def process( jsonp, messages, get )
57
+ replies = []
58
+ messages.each do |message|
59
+ reply = nil
60
+ case message['channel']
61
+ when RComet::Channel::HANDSHAKE
62
+ reply = handshake( message )
63
+ when RComet::Channel::CONNECT
64
+ reply = connect( message )
65
+ when RComet::Channel::DISCONNECT
66
+ reply = disconnect( message )
67
+ when RComet::Channel::SUBSCRIBE
68
+ reply = subscribe( message )
69
+ when RComet::Channel::UNSUBSCRIBE
70
+ reply = unsubscribe( message )
71
+ else
72
+ reply = handle( message )
73
+ end
74
+ if reply.class == Array
75
+ replies.concat( reply )
76
+ else
77
+ replies << reply
78
+ end
79
+ end
80
+
81
+ response = JSON.generate(replies)
82
+ type = {'Content-Type' => 'text/json'}
83
+ if get
84
+ response = "#{jsonp}(#{response});"
85
+ type = {'Content-Type' => 'text/javascript'}
86
+ end
87
+
88
+ [200, type, [response]]
89
+ end
90
+
91
+ def handshake( message )
92
+ begin
93
+ user = User.new(self)
94
+ end while @users.has_key?(user.id)
95
+ @users[user.id] = user
96
+
97
+ response = {
98
+ 'channel' => RComet::Channel::HANDSHAKE,
99
+ 'version' => RComet::BAYEUX_VERSION,
100
+ 'minimumVersion' => RComet::BAYEUX_VERSION,
101
+ 'supportedConnectionTypes' => ['long-polling','callback-polling'],
102
+ 'clientId' => user.id,
103
+ 'successful' => true
104
+ }
105
+ response << { 'id' => message['id'] } if message.has_key?('id')
106
+
107
+ return response
108
+ end
109
+
110
+ def connect( message )
111
+ # Initialize response
112
+ response = {
113
+ 'channel' => RComet::Channel::CONNECT,
114
+ 'clientId' => message['clientId']
115
+ }
116
+ response << { 'id' => message['id'] } if message.has_key?('id')
117
+
118
+ # Get user for clientId
119
+ user = @users[message['clientId']]
120
+ if user
121
+ # Ok, connect user
122
+ user.connected = true
123
+ time = Time.new
124
+ response << {
125
+ 'successful' => true,
126
+ 'timestamp' =>"#{time.hour}:#{time.min}:#{time.sec} #{time.year}"
127
+ }
128
+
129
+ if user.has_channel?
130
+ response = user.wait( response )
131
+ end
132
+ user.connected = false
133
+ else
134
+ # User does not exist!
135
+ response << {
136
+ 'successful' => false,
137
+ 'error' => "402:#{message['clientId']}:Unknown Client ID"
138
+ }
139
+ end
140
+
141
+ return response
142
+ end
143
+
144
+ def disconnect( message )
145
+ # Initialize response message
146
+ response = {
147
+ 'channel' => RComet::Channel::DISCONNECT,
148
+ 'clientId' => message['clientId']
149
+ }
150
+ response << { 'id' => message['id'] } if message.has_key?('id')
151
+
152
+ # Get user for clientId
153
+ user = @users[message['clientId']]
154
+ if user
155
+ # Ok, disconnect user
156
+ user.connected = false
157
+ @users.delete( message['clientId'] )
158
+
159
+ # Complete reponse
160
+ response << {
161
+ 'successful' => true
162
+ }
163
+ else
164
+ # User does nit exist!
165
+ response << {
166
+ 'successful' => false,
167
+ 'error' => "402:#{message['clientId']}:Unknown Client ID"
168
+ }
169
+ end
170
+
171
+ return response
172
+ end
173
+
174
+ def subscribe( message )
175
+ response = {
176
+ 'channel' => RComet::Channel::SUBSCRIBE,
177
+ 'clientId' => message['clientId']
178
+ }
179
+ response << { 'id' => message['id'] } if message.has_key?('id')
180
+
181
+ # Get user for clientId
182
+ user = @users[message['clientId']]
183
+ if user
184
+ # Get channel
185
+ channel = @channels[message['subscription']]
186
+ if channel
187
+ user.subscribe( channel )
188
+ response << {
189
+ 'successful' => true,
190
+ 'subscription' => message['subscription']
191
+ }
192
+
193
+ unless channel.data.nil?
194
+ response = [response]
195
+ response << {
196
+ 'channel' => message['subscription'],
197
+ 'id' => (message['id'].to_i+1).to_s,
198
+ 'data' => channel.data
199
+ }
200
+ end
201
+ else
202
+ #Channel doesn't exist
203
+ response << {
204
+ 'successful' => false,
205
+ 'subscription' => message['subscription'],
206
+ 'error' => "404:#{message['subscription']}:Unknown Channel"
207
+ }
208
+ end
209
+ else
210
+ response << {
211
+ 'successful' => false,
212
+ 'error' => "402:#{message['clientId']}:Unknown Client ID"
213
+ }
214
+ end
215
+
216
+ return response
217
+ end
218
+
219
+ def unsubscribe( message )
220
+ # Initialize response
221
+ response = {
222
+ 'channel' => RComet::Channel::UNSUBSCRIBE,
223
+ 'clientId' => message['clientId']
224
+ }
225
+ response << { 'id' => message['id'] } if message.has_key?('id')
226
+
227
+ # Get user for clientId
228
+ user = @users[message['clientId']]
229
+ if user
230
+ # Get channel
231
+ channel = @channels[message['subscription']]
232
+ if channel
233
+ user.unsubscribe( channel )
234
+ response << {
235
+ 'successful' => true,
236
+ 'subscription' => message['subscription']
237
+ }
238
+ else
239
+ #Channel doesn't exist
240
+ response << {
241
+ 'successful' => false,
242
+ 'subscription' => message['subscription'],
243
+ 'error' => "404:#{message['subscription']}:Unknown Channel"
244
+ }
245
+ end
246
+ else
247
+ response << {
248
+ 'successful' => false,
249
+ 'error' => "402:#{message['clientId']}:Unknown Client ID"
250
+ }
251
+ end
252
+
253
+ return response
254
+ end
255
+
256
+ def handle( message )
257
+ # Initialize response
258
+ response = {
259
+ 'channel' => message['channel']
260
+ }
261
+ response << { 'id' => message['id'] } if message.has_key?('id')
262
+
263
+ c = channel[message['channel']]
264
+ if c.nil?
265
+ response << {
266
+ 'successful' => false,
267
+ 'error' => "404:#{message['channel']}:Unknown Channel"
268
+ }
269
+ else
270
+ response << {
271
+ 'successful' => true
272
+ }
273
+ end
274
+
275
+ Thread.new do
276
+ unless c.nil?
277
+ if c.handler.nil?
278
+ c.data( message['data'] )
279
+ else
280
+ c.handler.call( message )
281
+ end
282
+ end
283
+ return
284
+ end
285
+
286
+ return response
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rcomet/rack_adapter'
3
+ require 'logger'
4
+
5
+ module RComet
6
+ class Server
7
+ # Create a new Comet server
8
+ def initialize( options = {}, &block )
9
+ @conf = {
10
+ :host => "0.0.0.0",
11
+ :port => 8990,
12
+ :mount => "/comet",
13
+ :log => $stdout,
14
+ :server => :webrick
15
+ }.merge( options )
16
+
17
+ if block_given?
18
+ @comet = RComet::RackAdapter.new( &block )
19
+ else
20
+ @comet = RComet::RackAdapter.new( )
21
+ end
22
+ end
23
+
24
+ # Start the Comet server
25
+ def start
26
+ route = { @conf[:mount] => @comet }
27
+ app = Rack::URLMap.new(route)
28
+ app = Rack::ContentLength.new(app)
29
+ app = Rack::CommonLogger.new(app, Logger.new(@conf[:log]))
30
+
31
+ @main_loop = Thread.new do
32
+ case @conf[:server].to_sym
33
+ when :mongrel
34
+ puts "** Starting Mongrel on #{@conf[:host]}:#{@conf[:port]}"
35
+ @server = Rack::Handler::Mongrel.run( app, {:Port => @conf[:port], :Host => @conf[:host]} )
36
+ when :webrick
37
+ puts "** Starting WEBrick on #{@conf[:host]}:#{@conf[:port]}"
38
+ @server = Rack::Handler::WEBrick.run( app, {:Port => @conf[:port], :BindAddress => @conf[:host]} )
39
+ when :thin
40
+ puts "** Starting Thin on #{@conf[:host]}:#{@conf[:port]}"
41
+ @server = Rack::Handler::Thin.run( app, {:Port => @conf[:port], :Host => @conf[:host]} )
42
+ end
43
+
44
+ puts "Thread end !"
45
+ exit
46
+ end
47
+
48
+ at_exit {
49
+ puts "Quitting..."
50
+ }
51
+ end
52
+
53
+ def method_missing(method_name, *args, &block) #:nodoc:
54
+ @comet.send(method_name,*args, &block)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,61 @@
1
+ module RComet
2
+ class User
3
+ attr_reader :id
4
+ attr_accessor :connected
5
+
6
+ # Create a new Comet user
7
+ def initialize(adapter)
8
+ @connected = false
9
+ @id = RComet.random(64)
10
+
11
+ @channels = Hash.new
12
+ @event_mutex = Mutex.new
13
+ @messages = []
14
+ @adapter = adapter
15
+ end
16
+
17
+ def wait( messages ) #:nodoc:
18
+ @continue = @messages.empty?
19
+ @messages << messages
20
+ while @continue; end
21
+
22
+ messages = @messages.clone
23
+ @messages = []
24
+ return messages
25
+ end
26
+
27
+ def send( message ) #:nodoc:
28
+ if @connected == false
29
+ ## ADD TIMEOUT !
30
+ unless @adapter.timeout.nil?
31
+ @adapter.timeout.call(self)
32
+ end
33
+ end
34
+
35
+ @messages << message
36
+ @continue = false
37
+ end
38
+
39
+ # Subscribe to a given channel
40
+ def subscribe( channel )
41
+ channel.add_user( self )
42
+ @channels[channel.path] = channel
43
+ end
44
+
45
+ # Unsubscribe to a given channel
46
+ def unsubscribe( channel )
47
+ c = @channels.delete(channel)
48
+ c.delete_user(self) if c
49
+ end
50
+
51
+ def unsubscribe_all( )
52
+ @channels.each do |c|
53
+ unsubscribe(c)
54
+ end
55
+ end
56
+
57
+ def has_channel? #:nodoc:
58
+ return( not @channels.empty? )
59
+ end
60
+ end
61
+ end
data/lib/rcomet.rb ADDED
@@ -0,0 +1,15 @@
1
+ module RComet
2
+ VERSION = '0.0.2'
3
+
4
+ BAYEUX_VERSION = '1.0'
5
+ JSONP_CALLBACK = 'jsonpcallback'
6
+ CONNECTION_TYPES = %w[long-polling callback-polling]
7
+
8
+ def self.random(size) #:nodoc:
9
+ id = ''
10
+ size.times do |i|
11
+ id << ?A+rand(50)
12
+ end
13
+ return id
14
+ end
15
+ end