bushido-faye 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/History.txt +247 -0
  2. data/README.rdoc +92 -0
  3. data/lib/faye-browser-min.js +1 -0
  4. data/lib/faye.rb +121 -0
  5. data/lib/faye/adapters/rack_adapter.rb +209 -0
  6. data/lib/faye/engines/connection.rb +60 -0
  7. data/lib/faye/engines/memory.rb +112 -0
  8. data/lib/faye/engines/proxy.rb +111 -0
  9. data/lib/faye/error.rb +49 -0
  10. data/lib/faye/mixins/logging.rb +47 -0
  11. data/lib/faye/mixins/publisher.rb +30 -0
  12. data/lib/faye/mixins/timeouts.rb +22 -0
  13. data/lib/faye/protocol/channel.rb +124 -0
  14. data/lib/faye/protocol/client.rb +378 -0
  15. data/lib/faye/protocol/extensible.rb +43 -0
  16. data/lib/faye/protocol/grammar.rb +58 -0
  17. data/lib/faye/protocol/publication.rb +5 -0
  18. data/lib/faye/protocol/server.rb +282 -0
  19. data/lib/faye/protocol/subscription.rb +24 -0
  20. data/lib/faye/transport/http.rb +76 -0
  21. data/lib/faye/transport/local.rb +22 -0
  22. data/lib/faye/transport/transport.rb +115 -0
  23. data/lib/faye/transport/web_socket.rb +99 -0
  24. data/lib/faye/util/namespace.rb +20 -0
  25. data/spec/browser.html +45 -0
  26. data/spec/encoding_helper.rb +7 -0
  27. data/spec/install.sh +78 -0
  28. data/spec/javascript/channel_spec.js +15 -0
  29. data/spec/javascript/client_spec.js +714 -0
  30. data/spec/javascript/engine/memory_spec.js +7 -0
  31. data/spec/javascript/engine_spec.js +417 -0
  32. data/spec/javascript/faye_spec.js +15 -0
  33. data/spec/javascript/grammar_spec.js +66 -0
  34. data/spec/javascript/node_adapter_spec.js +307 -0
  35. data/spec/javascript/publisher_spec.js +27 -0
  36. data/spec/javascript/server/connect_spec.js +168 -0
  37. data/spec/javascript/server/disconnect_spec.js +121 -0
  38. data/spec/javascript/server/extensions_spec.js +60 -0
  39. data/spec/javascript/server/handshake_spec.js +145 -0
  40. data/spec/javascript/server/integration_spec.js +124 -0
  41. data/spec/javascript/server/publish_spec.js +85 -0
  42. data/spec/javascript/server/subscribe_spec.js +247 -0
  43. data/spec/javascript/server/unsubscribe_spec.js +245 -0
  44. data/spec/javascript/server_spec.js +110 -0
  45. data/spec/javascript/transport_spec.js +130 -0
  46. data/spec/node.js +55 -0
  47. data/spec/phantom.js +17 -0
  48. data/spec/ruby/channel_spec.rb +17 -0
  49. data/spec/ruby/client_spec.rb +724 -0
  50. data/spec/ruby/engine/memory_spec.rb +7 -0
  51. data/spec/ruby/engine_examples.rb +427 -0
  52. data/spec/ruby/faye_spec.rb +14 -0
  53. data/spec/ruby/grammar_spec.rb +68 -0
  54. data/spec/ruby/publisher_spec.rb +27 -0
  55. data/spec/ruby/rack_adapter_spec.rb +236 -0
  56. data/spec/ruby/server/connect_spec.rb +170 -0
  57. data/spec/ruby/server/disconnect_spec.rb +120 -0
  58. data/spec/ruby/server/extensions_spec.rb +68 -0
  59. data/spec/ruby/server/handshake_spec.rb +143 -0
  60. data/spec/ruby/server/integration_spec.rb +126 -0
  61. data/spec/ruby/server/publish_spec.rb +81 -0
  62. data/spec/ruby/server/subscribe_spec.rb +247 -0
  63. data/spec/ruby/server/unsubscribe_spec.rb +247 -0
  64. data/spec/ruby/server_spec.rb +110 -0
  65. data/spec/ruby/transport_spec.rb +134 -0
  66. data/spec/spec_helper.rb +11 -0
  67. data/spec/testswarm +29 -0
  68. data/spec/thin_proxy.rb +37 -0
  69. metadata +302 -0
data/lib/faye/error.rb ADDED
@@ -0,0 +1,49 @@
1
+ module Faye
2
+ class Error
3
+
4
+ def self.method_missing(type, *args)
5
+ code = const_get(type.to_s.upcase)
6
+ new(code[0], args, code[1]).to_s
7
+ end
8
+
9
+ def self.parse(message)
10
+ message ||= ''
11
+ return new(nil, [], message) unless Grammar::ERROR =~ message
12
+
13
+ parts = message.split(':')
14
+ code = parts[0].to_i
15
+ params = parts[1].split(',')
16
+ message = parts[2]
17
+
18
+ new(code, params, message)
19
+ end
20
+
21
+ attr_reader :code, :params, :message
22
+
23
+ def initialize(code, params, message)
24
+ @code = code
25
+ @params = params
26
+ @message = message
27
+ end
28
+
29
+ def to_s
30
+ "#{ @code }:#{ @params * ',' }:#{ @message }"
31
+ end
32
+
33
+ # http://code.google.com/p/cometd/wiki/BayeuxCodes
34
+ VERSION_MISMATCH = [300, 'Version mismatch']
35
+ CONNTYPE_MISMATCH = [301, 'Connection types not supported']
36
+ EXT_MISMATCH = [302, 'Extension mismatch']
37
+ BAD_REQUEST = [400, 'Bad request']
38
+ CLIENT_UNKNOWN = [401, 'Unknown client']
39
+ PARAMETER_MISSING = [402, 'Missing required parameter']
40
+ CHANNEL_FORBIDDEN = [403, 'Forbidden channel']
41
+ CHANNEL_UNKNOWN = [404, 'Unknown channel']
42
+ CHANNEL_INVALID = [405, 'Invalid channel']
43
+ EXT_UNKNOWN = [406, 'Unknown extension']
44
+ PUBLISH_FAILED = [407, 'Failed to publish']
45
+ SERVER_ERROR = [500, 'Internal server error']
46
+
47
+ end
48
+ end
49
+
@@ -0,0 +1,47 @@
1
+ module Faye
2
+ module Logging
3
+
4
+ DEFAULT_LOG_LEVEL = :error
5
+
6
+ LOG_LEVELS = {
7
+ :error => 3,
8
+ :warn => 2,
9
+ :info => 1,
10
+ :debug => 0
11
+ }
12
+
13
+ class << self
14
+ attr_writer :log_level
15
+
16
+ def log_level
17
+ @log_level || DEFAULT_LOG_LEVEL
18
+ end
19
+ end
20
+
21
+ attr_writer :log_level
22
+
23
+ def log_level
24
+ @log_level || Logging.log_level
25
+ end
26
+
27
+ def log(message_args, level)
28
+ return unless Faye.logger
29
+ return if LOG_LEVELS[log_level] > LOG_LEVELS[level]
30
+
31
+ message = message_args.shift.gsub(/\?/) do
32
+ Faye.to_json(message_args.shift)
33
+ end
34
+
35
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
36
+ banner = " [#{ level.to_s.upcase }] [#{ self.class.name }] "
37
+
38
+ Faye.logger.call(timestamp + banner + message)
39
+ end
40
+
41
+ LOG_LEVELS.each do |level, value|
42
+ define_method(level) { |*args| log(args, level) }
43
+ end
44
+
45
+ end
46
+ end
47
+
@@ -0,0 +1,30 @@
1
+ module Faye
2
+ module Publisher
3
+
4
+ def count_listeners(event_type)
5
+ return 0 unless @subscribers and @subscribers[event_type]
6
+ @subscribers[event_type].size
7
+ end
8
+
9
+ def bind(event_type, &listener)
10
+ @subscribers ||= {}
11
+ list = @subscribers[event_type] ||= []
12
+ list << listener
13
+ end
14
+
15
+ def unbind(event_type, &listener)
16
+ return unless @subscribers and @subscribers[event_type]
17
+ return @subscribers.delete(event_type) unless listener
18
+
19
+ @subscribers[event_type].delete_if(&listener.method(:==))
20
+ end
21
+
22
+ def trigger(event_type, *args)
23
+ return unless @subscribers and @subscribers[event_type]
24
+ listeners = @subscribers[event_type].dup
25
+ listeners.each { |listener| listener.call(*args) }
26
+ end
27
+
28
+ end
29
+ end
30
+
@@ -0,0 +1,22 @@
1
+ module Faye
2
+ module Timeouts
3
+ def add_timeout(name, delay, &block)
4
+ Engine.ensure_reactor_running!
5
+ @timeouts ||= {}
6
+ return if @timeouts.has_key?(name)
7
+ @timeouts[name] = EventMachine.add_timer(delay) do
8
+ @timeouts.delete(name)
9
+ block.call
10
+ end
11
+ end
12
+
13
+ def remove_timeout(name)
14
+ @timeouts ||= {}
15
+ timeout = @timeouts[name]
16
+ return if timeout.nil?
17
+ EventMachine.cancel_timer(timeout)
18
+ @timeouts.delete(name)
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,124 @@
1
+ module Faye
2
+ class Channel
3
+
4
+ include Publisher
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+
11
+ def <<(message)
12
+ trigger(:message, message)
13
+ end
14
+
15
+ def unused?
16
+ count_listeners(:message).zero?
17
+ end
18
+
19
+ HANDSHAKE = '/meta/handshake'
20
+ CONNECT = '/meta/connect'
21
+ SUBSCRIBE = '/meta/subscribe'
22
+ UNSUBSCRIBE = '/meta/unsubscribe'
23
+ DISCONNECT = '/meta/disconnect'
24
+
25
+ META = :meta
26
+ SERVICE = :service
27
+
28
+ class << self
29
+ def expand(name)
30
+ segments = parse(name)
31
+ channels = ['/**', name]
32
+
33
+ copy = segments.dup
34
+ copy[copy.size - 1] = '*'
35
+ channels << unparse(copy)
36
+
37
+ 1.upto(segments.size - 1) do |i|
38
+ copy = segments[0...i]
39
+ copy << '**'
40
+ channels << unparse(copy)
41
+ end
42
+
43
+ channels
44
+ end
45
+
46
+ def valid?(name)
47
+ Grammar::CHANNEL_NAME =~ name or
48
+ Grammar::CHANNEL_PATTERN =~ name
49
+ end
50
+
51
+ def parse(name)
52
+ return nil unless valid?(name)
53
+ name.split('/')[1..-1].map { |s| s.to_sym }
54
+ end
55
+
56
+ def unparse(segments)
57
+ '/' + segments.join('/')
58
+ end
59
+
60
+ def meta?(name)
61
+ segments = parse(name)
62
+ segments ? (segments.first == META) : nil
63
+ end
64
+
65
+ def service?(name)
66
+ segments = parse(name)
67
+ segments ? (segments.first == SERVICE) : nil
68
+ end
69
+
70
+ def subscribable?(name)
71
+ return nil unless valid?(name)
72
+ not meta?(name) and not service?(name)
73
+ end
74
+ end
75
+
76
+ class Set
77
+ def initialize(parent = nil, value = nil)
78
+ @channels = {}
79
+ end
80
+
81
+ def keys
82
+ @channels.keys
83
+ end
84
+
85
+ def remove(name)
86
+ @channels.delete(name)
87
+ end
88
+
89
+ def has_subscription?(name)
90
+ @channels.has_key?(name)
91
+ end
92
+
93
+ def subscribe(names, callback)
94
+ return unless callback
95
+ names.each do |name|
96
+ channel = @channels[name] ||= Channel.new(name)
97
+ channel.bind(:message, &callback)
98
+ end
99
+ end
100
+
101
+ def unsubscribe(name, callback)
102
+ channel = @channels[name]
103
+ return false unless channel
104
+ channel.unbind(:message, &callback)
105
+ if channel.unused?
106
+ remove(name)
107
+ true
108
+ else
109
+ false
110
+ end
111
+ end
112
+
113
+ def distribute_message(message)
114
+ channels = Channel.expand(message['channel'])
115
+ channels.each do |name|
116
+ channel = @channels[name]
117
+ channel.trigger(:message, message['data']) if channel
118
+ end
119
+ end
120
+ end
121
+
122
+ end
123
+ end
124
+
@@ -0,0 +1,378 @@
1
+ module Faye
2
+ class Client
3
+
4
+ include EventMachine::Deferrable
5
+ include Publisher
6
+ include Logging
7
+ include Extensible
8
+
9
+ UNCONNECTED = 1
10
+ CONNECTING = 2
11
+ CONNECTED = 3
12
+ DISCONNECTED = 4
13
+
14
+ HANDSHAKE = 'handshake'
15
+ RETRY = 'retry'
16
+ NONE = 'none'
17
+
18
+ CONNECTION_TIMEOUT = 60.0
19
+ DEFAULT_RETRY = 5.0
20
+
21
+ attr_reader :endpoint, :client_id, :retry
22
+
23
+ def initialize(endpoint = nil, options = {})
24
+ info('New client created for ?', endpoint)
25
+
26
+ @endpoint = endpoint || RackAdapter::DEFAULT_ENDPOINT
27
+ @cookies = CookieJar::Jar.new
28
+ @headers = {}
29
+ @options = options
30
+ @disabled = []
31
+ @retry = @options[:retry] || DEFAULT_RETRY
32
+
33
+ select_transport(MANDATORY_CONNECTION_TYPES)
34
+
35
+ @state = UNCONNECTED
36
+ @channels = Channel::Set.new
37
+ @message_id = 0
38
+
39
+ @response_callbacks = {}
40
+
41
+ @advice = {
42
+ 'reconnect' => RETRY,
43
+ 'interval' => 1000.0 * (@options[:interval] || Engine::INTERVAL),
44
+ 'timeout' => 1000.0 * (@options[:timeout] || CONNECTION_TIMEOUT)
45
+ }
46
+ end
47
+
48
+ def disable(feature)
49
+ @disabled << feature
50
+ end
51
+
52
+ def set_header(name, value)
53
+ @headers[name.to_s] = value.to_s
54
+ end
55
+
56
+ def state
57
+ case @state
58
+ when UNCONNECTED then :UNCONNECTED
59
+ when CONNECTING then :CONNECTING
60
+ when CONNECTED then :CONNECTED
61
+ when DISCONNECTED then :DISCONNECTED
62
+ end
63
+ end
64
+
65
+ # Request
66
+ # MUST include: * channel
67
+ # * version
68
+ # * supportedConnectionTypes
69
+ # MAY include: * minimumVersion
70
+ # * ext
71
+ # * id
72
+ #
73
+ # Success Response Failed Response
74
+ # MUST include: * channel MUST include: * channel
75
+ # * version * successful
76
+ # * supportedConnectionTypes * error
77
+ # * clientId MAY include: * supportedConnectionTypes
78
+ # * successful * advice
79
+ # MAY include: * minimumVersion * version
80
+ # * advice * minimumVersion
81
+ # * ext * ext
82
+ # * id * id
83
+ # * authSuccessful
84
+ def handshake(&block)
85
+ return if @advice['reconnect'] == NONE
86
+ return if @state != UNCONNECTED
87
+
88
+ @state = CONNECTING
89
+
90
+ info('Initiating handshake with ?', @endpoint)
91
+
92
+ send({
93
+ 'channel' => Channel::HANDSHAKE,
94
+ 'version' => BAYEUX_VERSION,
95
+ 'supportedConnectionTypes' => [@transport.connection_type]
96
+
97
+ }) do |response|
98
+
99
+ if response['successful']
100
+ @state = CONNECTED
101
+ @client_id = response['clientId']
102
+
103
+ connection_types = response['supportedConnectionTypes'] - @disabled
104
+ select_transport(connection_types)
105
+
106
+ info('Handshake successful: ?', @client_id)
107
+
108
+ subscribe(@channels.keys, true)
109
+ block.call if block_given?
110
+
111
+ else
112
+ info('Handshake unsuccessful')
113
+ EventMachine.add_timer(@advice['interval'] / 1000.0) { handshake(&block) }
114
+ @state = UNCONNECTED
115
+ end
116
+ end
117
+ end
118
+
119
+ # Request Response
120
+ # MUST include: * channel MUST include: * channel
121
+ # * clientId * successful
122
+ # * connectionType * clientId
123
+ # MAY include: * ext MAY include: * error
124
+ # * id * advice
125
+ # * ext
126
+ # * id
127
+ # * timestamp
128
+ def connect(&block)
129
+ return if @advice['reconnect'] == NONE or
130
+ @state == DISCONNECTED
131
+
132
+ return handshake { connect(&block) } if @state == UNCONNECTED
133
+
134
+ callback(&block)
135
+ return unless @state == CONNECTED
136
+
137
+ info('Calling deferred actions for ?', @client_id)
138
+ set_deferred_status(:succeeded)
139
+ set_deferred_status(:deferred)
140
+
141
+ return unless @connect_request.nil?
142
+ @connect_request = true
143
+
144
+ info('Initiating connection for ?', @client_id)
145
+
146
+ send({
147
+ 'channel' => Channel::CONNECT,
148
+ 'clientId' => @client_id,
149
+ 'connectionType' => @transport.connection_type
150
+
151
+ }) do
152
+ cycle_connection
153
+ end
154
+ end
155
+
156
+ # Request Response
157
+ # MUST include: * channel MUST include: * channel
158
+ # * clientId * successful
159
+ # MAY include: * ext * clientId
160
+ # * id MAY include: * error
161
+ # * ext
162
+ # * id
163
+ def disconnect
164
+ return unless @state == CONNECTED
165
+ @state = DISCONNECTED
166
+
167
+ info('Disconnecting ?', @client_id)
168
+
169
+ send({
170
+ 'channel' => Channel::DISCONNECT,
171
+ 'clientId' => @client_id
172
+
173
+ }) do |response|
174
+ @transport.close if response['successful']
175
+ end
176
+
177
+ info('Clearing channel listeners for ?', @client_id)
178
+ @channels = Channel::Set.new
179
+ end
180
+
181
+ # Request Response
182
+ # MUST include: * channel MUST include: * channel
183
+ # * clientId * successful
184
+ # * subscription * clientId
185
+ # MAY include: * ext * subscription
186
+ # * id MAY include: * error
187
+ # * advice
188
+ # * ext
189
+ # * id
190
+ # * timestamp
191
+ def subscribe(channel, force = false, &block)
192
+ if Array === channel
193
+ return channel.each do |channel|
194
+ subscribe(channel, force, &block)
195
+ end
196
+ end
197
+
198
+ subscription = Subscription.new(self, channel, block)
199
+ has_subscribe = @channels.has_subscription?(channel)
200
+
201
+ if has_subscribe and not force
202
+ @channels.subscribe([channel], block)
203
+ subscription.set_deferred_status(:succeeded)
204
+ return subscription
205
+ end
206
+
207
+ connect {
208
+ info('Client ? attempting to subscribe to ?', @client_id, channel)
209
+ @channels.subscribe([channel], block) unless force
210
+
211
+ send({
212
+ 'channel' => Channel::SUBSCRIBE,
213
+ 'clientId' => @client_id,
214
+ 'subscription' => channel
215
+
216
+ }) do |response|
217
+ unless response['successful']
218
+ subscription.set_deferred_status(:failed, Error.parse(response['error']))
219
+ next @channels.unsubscribe(channel, block)
220
+ end
221
+
222
+ channels = [response['subscription']].flatten
223
+ info('Subscription acknowledged for ? to ?', @client_id, channels)
224
+ subscription.set_deferred_status(:succeeded)
225
+ end
226
+ }
227
+ subscription
228
+ end
229
+
230
+ # Request Response
231
+ # MUST include: * channel MUST include: * channel
232
+ # * clientId * successful
233
+ # * subscription * clientId
234
+ # MAY include: * ext * subscription
235
+ # * id MAY include: * error
236
+ # * advice
237
+ # * ext
238
+ # * id
239
+ # * timestamp
240
+ def unsubscribe(channel, &block)
241
+ if Array === channel
242
+ return channel.each do |channel|
243
+ unsubscribe(channel, &block)
244
+ end
245
+ end
246
+
247
+ dead = @channels.unsubscribe(channel, block)
248
+ return unless dead
249
+
250
+ connect {
251
+ info('Client ? attempting to unsubscribe from ?', @client_id, channel)
252
+
253
+ send({
254
+ 'channel' => Channel::UNSUBSCRIBE,
255
+ 'clientId' => @client_id,
256
+ 'subscription' => channel
257
+
258
+ }) do |response|
259
+ next unless response['successful']
260
+
261
+ channels = [response['subscription']].flatten
262
+ info('Unsubscription acknowledged for ? from ?', @client_id, channels)
263
+ end
264
+ }
265
+ end
266
+
267
+ # Request Response
268
+ # MUST include: * channel MUST include: * channel
269
+ # * data * successful
270
+ # MAY include: * clientId MAY include: * id
271
+ # * id * error
272
+ # * ext * ext
273
+ def publish(channel, data)
274
+ publication = Publication.new
275
+ connect {
276
+ info('Client ? queueing published message to ?: ?', @client_id, channel, data)
277
+
278
+ send({
279
+ 'channel' => channel,
280
+ 'data' => data,
281
+ 'clientId' => @client_id
282
+ }) do |response|
283
+ if response['successful']
284
+ publication.set_deferred_status(:succeeded)
285
+ else
286
+ publication.set_deferred_status(:failed, Error.parse(response['error']))
287
+ end
288
+ end
289
+ }
290
+ publication
291
+ end
292
+
293
+ def receive_message(message)
294
+ pipe_through_extensions(:incoming, message) do |message|
295
+ next unless message
296
+
297
+ handle_advice(message['advice']) if message['advice']
298
+ deliver_message(message)
299
+
300
+ next unless message.has_key?('successful')
301
+
302
+ callback = @response_callbacks[message['id']]
303
+ next unless callback
304
+
305
+ @response_callbacks.delete(message['id'])
306
+ callback.call(message)
307
+ end
308
+ end
309
+
310
+ private
311
+
312
+ def select_transport(transport_types)
313
+ Transport.get(self, transport_types) do |transport|
314
+ @transport = transport
315
+ @transport.cookies = @cookies
316
+ @transport.headers = @headers
317
+
318
+ transport.bind :down do
319
+ if @transport_up.nil? or @transport_up
320
+ @transport_up = false
321
+ trigger('transport:down')
322
+ end
323
+ end
324
+
325
+ transport.bind :up do
326
+ if @transport_up.nil? or not @transport_up
327
+ @transport_up = true
328
+ trigger('transport:up')
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ def send(message, &callback)
335
+ message['id'] = generate_message_id
336
+ @response_callbacks[message['id']] = callback if callback
337
+
338
+ pipe_through_extensions(:outgoing, message) do |message|
339
+ @transport.send(message, @advice['timeout'] / 1000.0) if message
340
+ end
341
+ end
342
+
343
+ def generate_message_id
344
+ @message_id += 1
345
+ @message_id = 0 if @message_id >= 2**32
346
+ @message_id.to_s(36)
347
+ end
348
+
349
+ def handle_advice(advice)
350
+ @advice.update(advice)
351
+
352
+ if @advice['reconnect'] == HANDSHAKE and @state != DISCONNECTED
353
+ @state = UNCONNECTED
354
+ @client_id = nil
355
+ cycle_connection
356
+ end
357
+ end
358
+
359
+ def deliver_message(message)
360
+ return unless message.has_key?('channel') and message.has_key?('data')
361
+ info('Client ? calling listeners for ? with ?', @client_id, message['channel'], message['data'])
362
+ @channels.distribute_message(message)
363
+ end
364
+
365
+ def teardown_connection
366
+ return unless @connect_request
367
+ @connect_request = nil
368
+ info('Closed connection for ?', @client_id)
369
+ end
370
+
371
+ def cycle_connection
372
+ teardown_connection
373
+ EventMachine.add_timer(@advice['interval'] / 1000.0) { connect }
374
+ end
375
+
376
+ end
377
+ end
378
+