rayeux 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+