rayeux 0.2.0

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,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Pete Schwamb
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # Rayeux: A ruby client to communicate with a comet server using Bayeux.
2
+
3
+ Provides client side functionality to talk to a Bayeux server such as the cometd server in Jetty.
4
+
5
+ Rayeux is heavily based on the javascript cometd library available here: http://cometdproject.dojotoolkit.org/
6
+
7
+ ## Install & Use:
8
+
9
+ * sudo gem install rayeux -s http://gemcutter.org
10
+ * See the chat_example.rb file for an example client that will talk to the chat demo included with Jetty (http://www.mortbay.org/jetty/)
11
+
12
+ ## Dependencies
13
+
14
+ * httpclient - http://github.com/nahi/httpclient
15
+
16
+ ## Todo
17
+
18
+ * Support ack extension
19
+ * Port to eventmachine-httpclient for a cleaner event driven model
20
+
21
+ ## Patches/Pull Requests
22
+
23
+ * This library currently pretty rough around the edges. Would love to know about any improvements or bug fixes.
24
+ * Feedback and comments are very welcome.
25
+
26
+ ## Copyright
27
+
28
+ Copyright (c) 2009 Pete Schwamb. See LICENSE for details.
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rayeux"
8
+ gem.summary = %Q{Ruby based Bayeux (comet) client implementation.}
9
+ gem.description = %Q{Provides client side functionality to talk to a Bayeux server such as the cometd server in Jetty.}
10
+ gem.email = "pete@schwamb.net"
11
+ gem.homepage = "http://github.com/ps2/rayeux"
12
+ gem.authors = ["Pete Schwamb"]
13
+ gem.add_development_dependency "httpclient", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "rayeux #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'rayeux'
4
+
5
+ class ChatClient
6
+ def initialize(url, name)
7
+ @name = name
8
+ @client = Rayeux::Client.new(url)
9
+ @connected = false
10
+ #@client.set_log_level('debug')
11
+
12
+ @client.add_listener('/meta/handshake') do |m|
13
+ @connected = false
14
+ end
15
+
16
+ @client.add_listener('/meta/connect') do |m|
17
+ meta_connect(m)
18
+ end
19
+ end
20
+
21
+ def run
22
+ @client.process_messages
23
+ end
24
+
25
+ def meta_connect(m)
26
+ was_connected = @connected
27
+ @connected = m["successful"]
28
+ if was_connected
29
+ if @connected
30
+ # Normal operation, a long poll that reconnects
31
+ else
32
+ # Disconnected
33
+ puts "Disconnected!"
34
+ end
35
+ else
36
+ if @connected
37
+ # Connected
38
+ puts "Connected!"
39
+ subscribe
40
+ else
41
+ # Could not connect
42
+ puts "Could not connect!"
43
+ end
44
+ end
45
+ end
46
+
47
+ def received_chat_message(from, text)
48
+ puts "Got chat demo message from #{from}: #{text}"
49
+ if text == 'ping'
50
+ send_chat_message('pong')
51
+ end
52
+ end
53
+
54
+ def send_chat_message(text)
55
+ @client.publish("/chat/demo", {
56
+ :user => @name,
57
+ :chat => text
58
+ })
59
+ end
60
+
61
+ def subscribe
62
+ @client.subscribe("/chat/demo") do |m|
63
+ if m["data"].is_a?(Hash)
64
+ received_chat_message(m["data"]["user"], m["data"]["chat"])
65
+ end
66
+ end
67
+
68
+ @client.publish("/chat/demo", {
69
+ :user => @name,
70
+ :join => true,
71
+ :chat => "#{@name} has joined"
72
+ })
73
+ end
74
+
75
+ end
76
+
77
+ client = ChatClient.new('http://localhost:8080/cometd/cometd', "rayeux")
78
+ client.run
@@ -0,0 +1,1007 @@
1
+ #!/usr/bin/env ruby
2
+ require "json"
3
+ require 'httpclient'
4
+
5
+ module Rayeux
6
+ class Transport
7
+
8
+ def initialize(type, http_client)
9
+ @in_queue = Queue.new
10
+ @out_queue = Queue.new
11
+ @num_threads = 2 # one for the long poll, another for requests
12
+ @type = type
13
+ @envelopes = []
14
+ @http = http_client
15
+ start_threads
16
+ end
17
+
18
+ def get_type
19
+ return @type
20
+ end
21
+
22
+ def get_response
23
+ @out_queue.pop
24
+ end
25
+
26
+ def start_threads
27
+ @num_threads.times do
28
+ Thread.new do
29
+ loop do
30
+ begin
31
+ envelope = @in_queue.pop
32
+ @out_queue.push(transport_send(envelope))
33
+ rescue
34
+ puts "Transport exception: " + e.message + " " + e.backtrace.join("\n")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def set_timeout(seconds)
42
+ @http.receive_timeout = seconds
43
+ end
44
+
45
+ def send_msg(envelope, longpoll)
46
+ @in_queue.push(envelope)
47
+ end
48
+
49
+ def complete(request, success, longpoll)
50
+ puts "** Completing #{request[:id]} longpoll=#{longpoll.inspect}"
51
+ #if longpoll
52
+ # longpoll_complete(request)
53
+ #else
54
+ # internal_complete(request, success)
55
+ #end
56
+ end
57
+
58
+ private
59
+
60
+ def abort
61
+ requests.each do |request|
62
+ # TODO
63
+ debug('Aborting request', request)
64
+ # if (request.xhr)
65
+ # request.xhr.abort();
66
+ #}
67
+ #if (_longpollRequest)
68
+ #{
69
+ # debug('Aborting request ', _longpollRequest);
70
+ # if (_longpollRequest.xhr) _longpollRequest.xhr.abort();
71
+ #}
72
+ #_longpollRequest = nil;
73
+ #_requests = [];
74
+ #_envelopes = [];
75
+ end
76
+ end
77
+ end
78
+
79
+ class LongPollingTransport < Transport
80
+ private
81
+ def transport_send(envelope)
82
+ begin
83
+ if envelope[:sleep]
84
+ sleep envelope[:sleep] / 1000.0
85
+ end
86
+ headers = {'Content-Type' => 'text/json;charset=UTF-8', 'X-Requested-With' => 'XMLHttpRequest'}
87
+ resp = @http.post(envelope[:url], envelope[:messages].to_json, headers)
88
+ envelope[:success] = resp.status == 200
89
+ envelope[:response] = resp
90
+ envelope[:reason] = resp.reason
91
+ #if resp.status != 200
92
+ # envelope[:on_failure].call(request, resp.reason, nil)
93
+ #else
94
+ # envelope[:on_success].call(request, resp)
95
+ #end
96
+ rescue Exception => e
97
+ envelope[:success] = false
98
+ envelope[:reason] = e.message
99
+ end
100
+ envelope
101
+ end
102
+ end
103
+
104
+ class CallbackPollingTransport < Transport
105
+ def transport_send(envelope)
106
+ raise "Not Implemented"
107
+ end
108
+ end
109
+
110
+ class Client
111
+ def initialize(configuration, name = nil, handshake_props = nil)
112
+ @message_id = 0
113
+ @name = name || 'default'
114
+ @log_level = 'warn' # 'warn','info','debug'
115
+ @status = 'disconnected'
116
+ @client_id = nil
117
+ @batch = 0
118
+ @message_queue = []
119
+ @listeners = {}
120
+ @backoff = 0
121
+ @scheduled_send = nil
122
+ @extensions = []
123
+ @advice = {}
124
+ @reestablish = false
125
+ @scheduled_send = nil
126
+
127
+ configure(configuration)
128
+ handshake(handshake_props)
129
+ end
130
+
131
+ def process_messages
132
+ while envelope = @transport.get_response
133
+ #puts "Received: #{envelope.inspect}"
134
+ if envelope[:success]
135
+ envelope[:on_success].call(envelope[:response])
136
+ else
137
+ envelope[:on_failure].call(envelope[:reason], nil)
138
+ end
139
+ end
140
+ end
141
+
142
+ # Adds a listener for bayeux messages, performing the given callback in the given scope
143
+ # when a message for the given channel arrives.
144
+ # channel: the channel the listener is interested to
145
+ # callback: the callback to call when a message is sent to the channel
146
+ # returns the subscription handle to be passed to {@link #removeListener(object)}
147
+ # @see #removeListener(object)
148
+ def add_listener(channel, &block)
149
+ internal_add_listener(channel, block, false)
150
+ end
151
+
152
+ # Find the extension registered with the given name.
153
+ # @param name the name of the extension to find
154
+ # @return the extension found or null if no extension with the given name has been registered
155
+ def get_extension(name)
156
+ @extensions.find {|e| e[:name] == name }
157
+ end
158
+
159
+ # Returns a string representing the status of the bayeux communication with the comet server.
160
+ def get_status
161
+ @status
162
+ end
163
+
164
+ # Starts a the batch of messages to be sent in a single request.
165
+ # see end_batch(send_messages)
166
+ def start_batch
167
+ @batch += 1
168
+ end
169
+
170
+ # Ends the batch of messages to be sent in a single request,
171
+ # optionally sending messages present in the message queue depending
172
+ # on the given argument.
173
+ # send_messages: whether to send the messages in the queue or not
174
+ # see start_batch
175
+ def end_batch(send_messages = true)
176
+ @batch -= 1
177
+ batch = 0 if @batch < 0
178
+ if send_messages && @batch == 0 && !is_disconnected
179
+ messages = @message_queue
180
+ message_queue = []
181
+ if messages.size > 0
182
+ internal_send(messages, false)
183
+ end
184
+ end
185
+ end
186
+
187
+ def next_message_id
188
+ @message_id += 1
189
+ end
190
+
191
+ # Subscribes to the given channel, calling the passed block
192
+ # when a message for the channel arrives.
193
+ # channel: the channel to subscribe to
194
+ # subscribe_props: an object to be merged with the subscribe message
195
+ # block: the block to call when a message is sent to the channel
196
+ # returns the subscription handle to be passed to #unsubscribe(object)
197
+ def subscribe(channel, subscribe_props = {}, &block)
198
+ # Only send the message to the server if this clientId has not yet subscribed to the channel
199
+ do_send = !has_subscriptions(channel)
200
+
201
+ subscription = internal_add_listener(channel, block, true)
202
+
203
+ if do_send
204
+ # Send the subscription message after the subscription registration to avoid
205
+ # races where the server would send a message to the subscribers, but here
206
+ # on the client the subscription has not been added yet to the data structures
207
+ bayeux_message = {
208
+ :channel => '/meta/subscribe',
209
+ :subscription => channel
210
+ }
211
+ message = subscribe_props.merge(bayeux_message)
212
+ queue_send(message)
213
+ end
214
+
215
+ return subscription
216
+ end
217
+
218
+ def has_subscriptions(channel)
219
+ subscriptions = @listeners[channel] || []
220
+ !subscriptions.empty?
221
+ end
222
+
223
+ # Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
224
+ # subscription: the subscription to unsubscribe.
225
+ def unsubscribe(subscription, unsubscribe_props)
226
+ # Remove the local listener before sending the message
227
+ # This ensures that if the server fails, this client does not get notifications
228
+ remove_listener(subscription)
229
+
230
+ channel = subscription[0]
231
+ # Only send the message to the server if this client_id unsubscribes the last subscription
232
+ if !has_subscriptions(channel)
233
+ bayeux_message = {
234
+ :channel => '/meta/unsubscribe',
235
+ :subscription => channel
236
+ }
237
+ message = unsubscribe_props.merge(bayeux_message)
238
+ queue_send(message)
239
+ end
240
+ end
241
+
242
+ # Publishes a message on the given channel, containing the given content.
243
+ # @param channel the channel to publish the message to
244
+ # @param content the content of the message
245
+ # @param publishProps an object to be merged with the publish message
246
+ def publish(channel, content, publish_props = {})
247
+ bayeux_message = {
248
+ :channel => channel,
249
+ :data => content
250
+ }
251
+ queue_send(publish_props.merge(bayeux_message))
252
+ end
253
+
254
+ # Sets the log level for console logging.
255
+ # Valid values are the strings 'error', 'warn', 'info' and 'debug', from
256
+ # less verbose to more verbose.
257
+ # @param level the log level string
258
+ def set_log_level(level)
259
+ @log_level = level
260
+ end
261
+
262
+ private
263
+
264
+ def internal_add_listener(channel, callback, is_subscription)
265
+ # The data structure is a map<channel, subscription[]>, where each subscription
266
+ # holds the callback to be called and its scope.
267
+
268
+ subscription = {
269
+ :callback => callback,
270
+ :subscription => is_subscription == true
271
+ };
272
+
273
+ subscriptions = @listeners[channel]
274
+ if !subscriptions
275
+ subscriptions = []
276
+ @listeners[channel] = subscriptions
277
+ end
278
+
279
+ subscriptions.push(subscription)
280
+ subscription_id = subscriptions.size
281
+
282
+ debug('internal_add_listener', channel, callback, subscription_id)
283
+
284
+ # The subscription to allow removal of the listener is made of the channel and the index
285
+ [channel, subscription_id]
286
+ end
287
+
288
+ # Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
289
+ # @param subscription the subscription to unsubscribe.
290
+ def remove_listener(subscription)
291
+ internal_remove_listener(subscription)
292
+ end
293
+
294
+ def internal_remove_listener(subscription)
295
+ subscriptions = @listeners[subscription[0]]
296
+ if subscriptions
297
+ subscriptions.delete_at(subscription[1])
298
+ debug('rm listener', subscription)
299
+ end
300
+ end
301
+
302
+ # Removes all listeners registered with add_listener(channel, scope, callback) or
303
+ # subscribe(channel, scope, callback).
304
+ def clear_listeners
305
+ @listeners = {}
306
+ end
307
+
308
+ # Removes all subscriptions added via {@link #subscribe(channel, scope, callback, subscribeProps)},
309
+ # but does not remove the listeners added via {@link add_listener(channel, scope, callback)}.
310
+ def clear_subscriptions
311
+ internal_clear_subscriptions
312
+ end
313
+
314
+ def clear_subscriptions
315
+ @listeners.each do |channel,subscriptions|
316
+ debug('rm subscriptions', channel, subscriptions)
317
+ subscriptions.delete_if {|s| s[:subscription]}
318
+ end
319
+ end
320
+
321
+
322
+ # Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
323
+ # Default value is 1 second, which means if there is a persistent failure the retries will happen
324
+ # after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
325
+ # elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
326
+ # @param period the backoff period to set
327
+ # @see #getBackoffIncrement()
328
+
329
+ def set_backoff_increment(period)
330
+ @backoff_increment = period
331
+ end
332
+
333
+ # Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
334
+ # @see #setBackoffIncrement(period)
335
+ def get_backoff_increment
336
+ @backoff_increment
337
+ end
338
+
339
+ # Returns the backoff period to wait before retrying an unsuccessful or failed message.
340
+ def get_backoff_period
341
+ @backoff
342
+ end
343
+
344
+ def increase_backoff
345
+ if @backoff < @max_backoff
346
+ @backoff += @backoff_increment
347
+ end
348
+ end
349
+
350
+ # Registers an extension whose callbacks are called for every incoming message
351
+ # (that comes from the server to this client implementation) and for every
352
+ # outgoing message (that originates from this client implementation for the
353
+ # server).
354
+ # The format of the extension object is the following:
355
+ # <pre>
356
+ # {
357
+ # incoming: function(message) { ... },
358
+ # outgoing: function(message) { ... }
359
+ # }
360
+ # </pre>
361
+ # Both properties are optional, but if they are present they will be called
362
+ # respectively for each incoming message and for each outgoing message.
363
+ # @param name the name of the extension
364
+ # @param extension the extension to register
365
+ # @return true if the extension was registered, false otherwise
366
+ # @see #unregisterExtension(name)
367
+
368
+ def register_extension(name, extension)
369
+ existing = extensions.any? {|e| e[:name] == name}
370
+
371
+ if !existing
372
+ @extensions.push({
373
+ :name => name,
374
+ :extension => extension
375
+ })
376
+ debug('Registered extension', name)
377
+
378
+ # Callback for extensions
379
+ if extension[:registered]
380
+ extension[:registered].call(extension, name, this)
381
+ end
382
+
383
+ true
384
+ else
385
+ debug('Could not register extension with name \'{}\': another extension with the same name already exists');
386
+ false
387
+ end
388
+ end
389
+
390
+
391
+ # Unregister an extension previously registered with
392
+ # {@link #registerExtension(name, extension)}.
393
+ # @param name the name of the extension to unregister.
394
+ # @return true if the extension was unregistered, false otherwise
395
+ def unregister_extension(name)
396
+ unregistered = false
397
+ @extensions.delete_if do |extension|
398
+ if extension[:name] == name
399
+ unregistered = true
400
+ if extension[:unregistered]
401
+ extension[:unregistered].call(extension)
402
+ end
403
+ true
404
+ end
405
+ end
406
+ unregistered
407
+ end
408
+
409
+ def next_message_id
410
+ @message_id += 1
411
+ @message_id - 1
412
+ end
413
+
414
+
415
+ # Converts the given response into an array of bayeux messages
416
+ # @param response the response to convert
417
+ # @return an array of bayeux messages obtained by converting the response
418
+
419
+ def convert_to_messages(response)
420
+ json = JSON.parse(response.content)
421
+ end
422
+
423
+ def set_status(new_status)
424
+ debug('status',@status,'->',new_status);
425
+ @status = new_status
426
+ end
427
+
428
+ def is_disconnected
429
+ @status == 'disconnecting' || @status == 'disconnected'
430
+ end
431
+
432
+ def handshake(handshake_props, delay=nil)
433
+ debug 'handshake'
434
+ @client_id = nil
435
+
436
+ clear_subscriptions
437
+
438
+ # Start a batch.
439
+ # This is needed because handshake and connect are async.
440
+ # It may happen that the application calls init() then subscribe()
441
+ # and the subscribe message is sent before the connect message, if
442
+ # the subscribe message is not held until the connect message is sent.
443
+ # So here we start a batch to hold temporarly any message until
444
+ # the connection is fully established.
445
+ @batch = 0;
446
+ start_batch
447
+
448
+ handshake_props ||= {}
449
+
450
+ bayeux_message = {
451
+ :version => '1.0',
452
+ :minimumVersion => '0.9',
453
+ :channel => '/meta/handshake',
454
+ :supportedConnectionTypes => ['long-polling', 'callback-polling']
455
+ }
456
+
457
+ # Do not allow the user to mess with the required properties,
458
+ # so merge first the user properties and *then* the bayeux message
459
+ message = handshake_props.merge(bayeux_message)
460
+
461
+ # We started a batch to hold the application messages,
462
+ # so here we must bypass it and send immediately.
463
+ set_status('handshaking')
464
+ debug('handshake send',message)
465
+ internal_send([message], false, delay)
466
+ end
467
+
468
+ def configure(configuration)
469
+ debug('configure cometd ', configuration)
470
+ # Support old style param, where only the comet URL was passed
471
+ if configuration.is_a?(String) || configuration.is_a?(URI::HTTP)
472
+ configuration = { :url => configuration }
473
+ end
474
+
475
+ configuration ||= {}
476
+
477
+ @url = configuration[:url]
478
+ if @url.nil?
479
+ raise "Missing required configuration parameter 'url' specifying the comet server URL"
480
+ end
481
+
482
+ @http = configuration[:http_client] || HTTPClient.new
483
+
484
+ @backoff_increment = configuration[:backoff_increment] || 1000
485
+ @max_backoff = configuration[:max_backoff] || 60000
486
+ @log_level = configuration[:log_level] || 'info'
487
+ @reverse_incoming_extensions = configuration[:reverse_incoming_extensions] != false
488
+
489
+ # Temporary setup a transport to send the initial handshake
490
+ # The transport may be changed as a result of handshake
491
+ @transport = new_long_polling_transport
492
+ debug('transport', @transport)
493
+ end
494
+
495
+ def new_long_polling_transport
496
+ LongPollingTransport.new("long-polling", @http)
497
+ end
498
+
499
+ def internal_send(messages, long_poll, delay = nil)
500
+ # We must be sure that the messages have a clientId.
501
+ # This is not guaranteed since the handshake may take time to return
502
+ # (and hence the clientId is not known yet) and the application
503
+ # may create other messages.
504
+ messages = messages.map do |message|
505
+ message['id'] = next_message_id
506
+ message['clientId'] = @client_id if @client_id
507
+ message = apply_outgoing_extensions(message)
508
+ end
509
+ messages.compact!
510
+
511
+ return if messages.empty?
512
+
513
+ success_callback = lambda do |response|
514
+ begin
515
+ handle_response(response, long_poll)
516
+ rescue Exception => x
517
+ warn("handle_response exception", x, x.backtrace.join("\n") )
518
+ end
519
+ end
520
+
521
+ failure_callback = lambda do |reason, exception|
522
+ begin
523
+ handle_failure(messages, reason, exception, long_poll)
524
+ rescue Exception => x
525
+ warn("handle_failure exception: ", x.inspect, x.backtrace.join("\n"))
526
+ end
527
+ end
528
+
529
+ #var self = this;
530
+ envelope = {
531
+ :url => @url,
532
+ :sleep => delay,
533
+ :messages => messages,
534
+ :on_success => success_callback,
535
+ :on_failure => failure_callback
536
+ }
537
+ debug('internal_send', envelope)
538
+ @transport.send_msg(envelope, long_poll)
539
+ end
540
+
541
+ def debug(msg, *rest)
542
+ if @log_level == 'debug'
543
+ puts msg + ": " + rest.map {|r| r.inspect} .join(", ")
544
+ end
545
+ end
546
+
547
+ def warn(msg, *rest)
548
+ puts "Warning: " + msg.inspect + ": " + rest.map {|r| r.inspect} .join(", ")
549
+ end
550
+
551
+ def receive(message)
552
+ if message["advice"]
553
+ @advice = message["advice"]
554
+ end
555
+
556
+ channel = message["channel"]
557
+ case channel
558
+ when '/meta/handshake'
559
+ handshake_response(message)
560
+ when '/meta/connect'
561
+ connect_response(message)
562
+ when '/meta/disconnect'
563
+ disconnect_response(message)
564
+ when '/meta/subscribe'
565
+ subscribe_response(message)
566
+ when '/meta/unsubscribe'
567
+ unsubscribe_response(message)
568
+ else
569
+ message_response(message)
570
+ end
571
+ end
572
+
573
+ def handle_response(response, longpoll)
574
+ messages = convert_to_messages(response)
575
+ debug('Received', messages)
576
+
577
+ messages = messages.map {|m| apply_incoming_extensions(m) }
578
+ messages.compact!
579
+ messages.each {|m| receive(m)}
580
+ end
581
+
582
+ def apply_incoming_extensions(message)
583
+ @extensions.each do |extension|
584
+ callback = extension[:extension][:incoming]
585
+ if (callback)
586
+ message = apply_extension(extension[:name], callback, message)
587
+ end
588
+ end
589
+ message
590
+ end
591
+
592
+ def apply_outgoing_extensions(message)
593
+ @extensions.each do |extension|
594
+ callback = extension[:extension][:outgoing]
595
+ if (callback)
596
+ message = apply_extension(extension[:name], callback, message)
597
+ end
598
+ end
599
+ message
600
+ end
601
+
602
+ def apply_extension(name, callback, message)
603
+ begin
604
+ message = callback.call(message)
605
+ rescue Exception => e
606
+ warn("extension exception", e.message, e.backtrace.join("\n"))
607
+ end
608
+ message
609
+ end
610
+
611
+ def handle_failure(messages, reason, exception, longpoll)
612
+
613
+ debug('Failed', messages)
614
+
615
+ messages.each do |message|
616
+ channel = message[:channel]
617
+ puts "processing failed message on channel: #{channel}"
618
+ case channel
619
+ when '/meta/handshake'
620
+ handshake_failure(message)
621
+ when '/meta/connect'
622
+ connect_failure(message)
623
+ when '/meta/disconnect'
624
+ disconnect_failure(message)
625
+ when '/meta/subscribe'
626
+ subscribe_failure(message)
627
+ when '/meta/unsubscribe'
628
+ unsubscribe_failure(message)
629
+ else
630
+ message_failure(message)
631
+ end
632
+ end
633
+ end
634
+
635
+ def handshake_response(message)
636
+ if message["successful"]
637
+ # Save clientId, figure out transport, then follow the advice to connect
638
+ @client_id = message["clientId"]
639
+
640
+ new_transport = find_transport(message)
641
+ if new_transport.nil?
642
+ raise 'Could not agree on transport with server'
643
+ elsif @transport.get_type != new_transport.get_type
644
+ debug('transport', @transport, '->', new_transport)
645
+ @transport = new_transport
646
+ end
647
+
648
+ # Notify the listeners
649
+ # Here the new transport is in place, as well as the clientId, so
650
+ # the listener can perform a publish() if it wants, and the listeners
651
+ # are notified before the connect below.
652
+ message[:reestablish] = @reestablish
653
+ @reestablish = true
654
+ notify_listeners('/meta/handshake', message)
655
+
656
+ action = @advice["reconnect"] || 'retry'
657
+ if action == 'retry'
658
+ delayed_connect
659
+ end
660
+
661
+ else
662
+ should_retry = !is_disconnected && (@advice["reconnect"] != 'none')
663
+ if !should_retry
664
+ set_status('disconnected')
665
+ end
666
+
667
+ notify_listeners('/meta/handshake', message)
668
+ notify_listeners('/meta/unsuccessful', message)
669
+
670
+ # Only try again if we haven't been disconnected and
671
+ # the advice permits us to retry the handshake
672
+ if should_retry
673
+ increase_backoff
674
+ delayed_handshake
675
+ end
676
+ end
677
+ end
678
+
679
+ def handshake_failure(message)
680
+ # Notify listeners
681
+ failure_message = {
682
+ :successful => false,
683
+ :failure => true,
684
+ :channel => '/meta/handshake',
685
+ :request => message,
686
+ :advice => {
687
+ :action => 'retry',
688
+ :interval => @backoff
689
+ }
690
+ }
691
+
692
+ should_retry = !is_disconnected && @advice["reconnect"] != 'none'
693
+ if !should_retry
694
+ set_status('disconnected')
695
+ end
696
+
697
+ notify_listeners('/meta/handshake', failure_message)
698
+ notify_listeners('/meta/unsuccessful', failure_message)
699
+
700
+ # Only try again if we haven't been disconnected and the
701
+ # advice permits us to try again
702
+ if should_retry
703
+ increase_backoff
704
+ delayed_handshake
705
+ end
706
+ end
707
+
708
+ def find_transport(handshake_response)
709
+ transport_types = handshake_response["supportedConnectionTypes"]
710
+ # Check if we can keep long-polling
711
+ if transport_types.include?('long-polling')
712
+ @transport
713
+ elsif transportTypes.include?('callback-polling')
714
+ new_callback_polling_transport
715
+ else
716
+ nil
717
+ end
718
+ end
719
+
720
+ def delayed_handshake
721
+ set_status('handshaking')
722
+ handshake(@handshake_props, get_next_delay)
723
+ end
724
+
725
+ def delayed_connect
726
+ set_status('connecting')
727
+ internal_connect(get_next_delay)
728
+ end
729
+
730
+ def get_next_delay
731
+ delay = @backoff
732
+ if @advice["interval"] && @advice["interval"].to_f > 0
733
+ delay += @advice["interval"]
734
+ end
735
+ delay
736
+ end
737
+
738
+ def internal_connect(delay = nil)
739
+ debug('connect')
740
+ message = {
741
+ :channel => '/meta/connect',
742
+ :connectionType => @transport.get_type
743
+ }
744
+ set_status('connecting')
745
+ internal_send([message], true, delay)
746
+ set_status('connected')
747
+ end
748
+
749
+ def queue_send(message)
750
+ if (@batch > 0)
751
+ @message_queue.push(message)
752
+ else
753
+ internal_send([message], false)
754
+ end
755
+ end
756
+
757
+ def connect_response(message)
758
+ action = is_disconnected ? 'none' : (@advice["reconnect"] || 'retry')
759
+ if !is_disconnected
760
+ set_status(action == 'retry' ? 'connecting' : 'disconnecting')
761
+ end
762
+
763
+ if @advice["timeout"]
764
+ # Set transport level timeout to comet timeout + 10 seconds
765
+ @transport.set_timeout(@advice["timeout"].to_i / 1000.0 + 10)
766
+ end
767
+
768
+ if message["successful"]
769
+ # End the batch and allow held messages from the application
770
+ # to go to the server (see _handshake() where we start the batch).
771
+ # The batch is ended before notifying the listeners, so that
772
+ # listeners can batch other cometd operations
773
+ end_batch(true)
774
+
775
+ # Notify the listeners after the status change but before the next connect
776
+ notify_listeners('/meta/connect', message)
777
+
778
+ # Connect was successful.
779
+ # Normally, the advice will say "reconnect: 'retry', interval: 0"
780
+ # and the server will hold the request, so when a response returns
781
+ # we immediately call the server again (long polling)
782
+ case action
783
+ when 'retry':
784
+ reset_backoff
785
+ delayed_connect
786
+ else
787
+ reset_backoff
788
+ set_status('disconnected')
789
+ end
790
+
791
+ else
792
+ # Notify the listeners after the status change but before the next action
793
+ notify_listeners('/meta/connect', message)
794
+ notify_listeners('/meta/unsuccessful', message)
795
+
796
+ # Connect was not successful.
797
+ # This may happen when the server crashed, the current clientId
798
+ # will be invalid, and the server will ask to handshake again
799
+ case action
800
+ when 'retry'
801
+ increase_backoff
802
+ delayed_connect
803
+ when 'handshake'
804
+ # End the batch but do not send the messages until we connect successfully
805
+ end_batch(false)
806
+ reset_backoff
807
+ delayed_handshake
808
+ when 'none':
809
+ reset_backoff
810
+ set_status('disconnected')
811
+ end
812
+ end
813
+ end
814
+
815
+ def connect_failure(message)
816
+ debug("connect failure", message)
817
+
818
+ # Notify listeners
819
+ failure_message = {
820
+ :successful => false,
821
+ :failure => true,
822
+ :channel => '/meta/connect',
823
+ :request => message,
824
+ :advice => {
825
+ :action => 'retry',
826
+ :interval => @backoff
827
+ }
828
+ }
829
+ notify_listeners('/meta/connect', failure_message)
830
+ notify_listeners('/meta/unsuccessful', failure_message)
831
+
832
+ if !is_disconnected
833
+ action = @advice["reconnect"] ? @advice["reconnect"] : 'retry'
834
+ case action
835
+ when 'retry'
836
+ increase_backoff
837
+ delayed_connect
838
+ when 'handshake'
839
+ reset_backoff
840
+ delayed_handshake
841
+ when 'none'
842
+ reset_backoff
843
+ else
844
+ debug('Unrecognized action', action)
845
+ end
846
+ end
847
+ end
848
+
849
+ def disconnect_response(message)
850
+ if message["successful"]
851
+ disconnect(false)
852
+ notify_listeners('/meta/disconnect', message)
853
+ else
854
+ disconnect(true)
855
+ notify_listeners('/meta/disconnect', message)
856
+ notify_listeners('/meta/usuccessful', message)
857
+ end
858
+ end
859
+
860
+ def disconnect(abort)
861
+ cancel_delayed_send
862
+ if abort
863
+ @transport.abort
864
+ end
865
+ @client_id = nil
866
+ set_status('disconnected')
867
+ @batch = 0
868
+ @messageQueue = []
869
+ reset_backoff
870
+ end
871
+
872
+ def disconnect_failure(message)
873
+ disconnect(true)
874
+
875
+ failure_message = {
876
+ :successful => false,
877
+ :failure => true,
878
+ :channel => '/meta/disconnect',
879
+ :request => message,
880
+ :advice => {
881
+ :action => 'none',
882
+ :interval => 0
883
+ }
884
+ }
885
+ notify_listeners('/meta/disconnect', failure_message)
886
+ notify_listeners('/meta/unsuccessful', failure_message)
887
+ end
888
+
889
+ def subscribe_response(message)
890
+ if message["successful"]
891
+ notify_listeners('/meta/subscribe', message)
892
+ else
893
+ notify_listeners('/meta/subscribe', message)
894
+ notify_listeners('/meta/unsuccessful', message)
895
+ end
896
+ end
897
+
898
+ def subscribe_failure(message)
899
+ failure_message = {
900
+ :successful => false,
901
+ :failure => true,
902
+ :channel => '/meta/subscribe',
903
+ :request => message,
904
+ :advice => {
905
+ :action => 'none',
906
+ :interval => 0
907
+ }
908
+ }
909
+ notify_listeners('/meta/subscribe', failure_message)
910
+ notify_listeners('/meta/unsuccessful', failure_message)
911
+ end
912
+
913
+ def unsubscribe_response(message)
914
+ if message["successful"]
915
+ notify_listeners('/meta/unsubscribe', message)
916
+ else
917
+ notify_listeners('/meta/unsubscribe', message)
918
+ notify_listeners('/meta/unsuccessful', message)
919
+ end
920
+ end
921
+
922
+ def unsubscribe_failure(message)
923
+ failure_message = {
924
+ :successful => false,
925
+ :failure => true,
926
+ :channel => '/meta/unsubscribe',
927
+ :request => message,
928
+ :advice => {
929
+ :action => 'none',
930
+ :interval => 0
931
+ }
932
+ }
933
+ notify_listeners('/meta/unsubscribe', failure_message)
934
+ notify_listeners('/meta/unsuccessful', failure_message)
935
+ end
936
+
937
+ def message_response(message)
938
+ if message["successful"].nil?
939
+ if message["data"]
940
+ # It is a plain message, and not a bayeux meta message
941
+ notify_listeners(message["channel"], message)
942
+ else
943
+ debug('Unknown message', message)
944
+ end
945
+ else
946
+ if message["successful"]
947
+ notify_listeners('/meta/publish', message)
948
+ else
949
+ notify_listeners('/meta/publish', message)
950
+ notify_listeners('/meta/unsuccessful', message)
951
+ end
952
+ end
953
+ end
954
+
955
+ def message_failure(message)
956
+ failure_message = {
957
+ :successful => false,
958
+ :failure => true,
959
+ :channel => message["channel"],
960
+ :request => message,
961
+ :advice => {
962
+ :action => 'none',
963
+ :interval => 0
964
+ }
965
+ }
966
+ notify_listeners('/meta/publish', failure_message)
967
+ notify_listeners('/meta/unsuccessful', failure_message)
968
+ end
969
+
970
+ def notify_listeners(channel, message)
971
+ # Notify direct listeners
972
+ notify(channel, message)
973
+
974
+ # Notify the globbing listeners
975
+ channel_parts = channel.split("/");
976
+ last = channel_parts.size - 1;
977
+ last.downto(0) do |i|
978
+ channel_part = channel_parts.slice(0, i).join('/') + '/*';
979
+ # We don't want to notify /foo/* if the channel is /foo/bar/baz,
980
+ # so we stop at the first non recursive globbing
981
+ if (i == last)
982
+ notify(channel_part, message)
983
+ end
984
+ # Add the recursive globber and notify
985
+ channel_part += '*'
986
+ notify(channel_part, message)
987
+ end
988
+ end
989
+
990
+ def notify(channel, message)
991
+ subscriptions = @listeners[channel] || []
992
+ subscriptions.compact.each do |subscription|
993
+ begin
994
+ subscription[:callback].call(message);
995
+ rescue Exception => e
996
+ warn(subscription,message,e)
997
+ end
998
+ end
999
+ end
1000
+
1001
+ def reset_backoff
1002
+ @backoff = 0
1003
+ end
1004
+
1005
+ end
1006
+ end
1007
+