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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.markdown +28 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/chat_example.rb +78 -0
- data/lib/rayeux.rb +1007 -0
- data/rayeux.gemspec +55 -0
- data/test/helper.rb +10 -0
- data/test/test_rayeux.rb +25 -0
- metadata +76 -0
data/.document
ADDED
data/.gitignore
ADDED
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.
|
data/README.markdown
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/chat_example.rb
ADDED
@@ -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
|
data/lib/rayeux.rb
ADDED
@@ -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
|
+
|