faye 0.6.8 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of faye might be problematic. Click here for more details.

Files changed (43) hide show
  1. data/History.txt +10 -3
  2. data/README.rdoc +1 -2
  3. data/lib/faye-browser-min.js +1 -1
  4. data/lib/faye.rb +89 -32
  5. data/lib/faye/adapters/rack_adapter.rb +20 -26
  6. data/lib/faye/engines/base.rb +5 -0
  7. data/lib/faye/engines/memory.rb +9 -3
  8. data/lib/faye/engines/redis.rb +26 -11
  9. data/lib/faye/mixins/publisher.rb +4 -8
  10. data/lib/faye/protocol/channel.rb +8 -8
  11. data/lib/faye/protocol/client.rb +45 -4
  12. data/lib/faye/protocol/publication.rb +5 -0
  13. data/lib/faye/protocol/server.rb +10 -19
  14. data/lib/faye/thin_extensions.rb +1 -1
  15. data/lib/faye/transport/http.rb +17 -8
  16. data/lib/faye/transport/local.rb +6 -3
  17. data/lib/faye/transport/transport.rb +23 -9
  18. data/lib/faye/transport/web_socket.rb +102 -0
  19. data/lib/faye/util/web_socket.rb +34 -80
  20. data/lib/faye/util/web_socket/api.rb +103 -0
  21. data/lib/faye/util/web_socket/client.rb +82 -0
  22. data/lib/faye/util/web_socket/draft75_parser.rb +3 -5
  23. data/lib/faye/util/web_socket/draft76_parser.rb +5 -7
  24. data/lib/faye/util/web_socket/protocol8_parser.rb +111 -46
  25. data/spec/javascript/client_spec.js +99 -7
  26. data/spec/javascript/engine_spec.js +116 -3
  27. data/spec/javascript/node_adapter_spec.js +2 -4
  28. data/spec/javascript/server/handshake_spec.js +0 -12
  29. data/spec/javascript/server/integration_spec.js +74 -29
  30. data/spec/javascript/server_spec.js +0 -11
  31. data/spec/javascript/web_socket/client_spec.js +121 -0
  32. data/spec/javascript/web_socket/protocol8parser_spec.js +26 -3
  33. data/spec/node.js +2 -0
  34. data/spec/redis.conf +10 -280
  35. data/spec/ruby/client_spec.rb +101 -8
  36. data/spec/ruby/engine_spec.rb +106 -0
  37. data/spec/ruby/server/handshake_spec.rb +0 -12
  38. data/spec/ruby/server/integration_spec.rb +56 -18
  39. data/spec/ruby/server_spec.rb +1 -12
  40. data/spec/ruby/transport_spec.rb +14 -8
  41. data/spec/ruby/web_socket/client_spec.rb +126 -0
  42. data/spec/ruby/web_socket/protocol8_parser_spec.rb +28 -3
  43. metadata +96 -150
@@ -9,11 +9,11 @@ module Faye
9
9
  end
10
10
 
11
11
  def <<(message)
12
- publish_event(:message, message)
12
+ trigger(:message, message)
13
13
  end
14
14
 
15
15
  def unused?
16
- count_subscribers(:message).zero?
16
+ count_listeners(:message).zero?
17
17
  end
18
18
 
19
19
  HANDSHAKE = '/meta/handshake'
@@ -22,8 +22,8 @@ module Faye
22
22
  UNSUBSCRIBE = '/meta/unsubscribe'
23
23
  DISCONNECT = '/meta/disconnect'
24
24
 
25
- META = 'meta'
26
- SERVICE = 'service'
25
+ META = :meta
26
+ SERVICE = :service
27
27
 
28
28
  class << self
29
29
  def expand(name)
@@ -50,7 +50,7 @@ module Faye
50
50
 
51
51
  def parse(name)
52
52
  return nil unless valid?(name)
53
- name.split('/')[1..-1]
53
+ name.split('/')[1..-1].map { |s| s.to_sym }
54
54
  end
55
55
 
56
56
  def unparse(segments)
@@ -94,14 +94,14 @@ module Faye
94
94
  return unless callback
95
95
  names.each do |name|
96
96
  channel = @channels[name] ||= Channel.new(name)
97
- channel.add_subscriber(:message, callback)
97
+ channel.bind(:message, &callback)
98
98
  end
99
99
  end
100
100
 
101
101
  def unsubscribe(name, callback)
102
102
  channel = @channels[name]
103
103
  return false unless channel
104
- channel.remove_subscriber(:message, callback)
104
+ channel.unbind(:message, &callback)
105
105
  if channel.unused?
106
106
  remove(name)
107
107
  true
@@ -114,7 +114,7 @@ module Faye
114
114
  channels = Channel.expand(message['channel'])
115
115
  channels.each do |name|
116
116
  channel = @channels[name]
117
- channel.publish_event(:message, message['data']) if channel
117
+ channel.trigger(:message, message['data']) if channel
118
118
  end
119
119
  end
120
120
  end
@@ -2,6 +2,7 @@ module Faye
2
2
  class Client
3
3
 
4
4
  include EventMachine::Deferrable
5
+ include Publisher
5
6
  include Logging
6
7
  include Extensible
7
8
 
@@ -16,6 +17,7 @@ module Faye
16
17
 
17
18
  CONNECTION_TIMEOUT = 60.0
18
19
 
20
+ attr_accessor :cookies
19
21
  attr_reader :endpoint, :client_id
20
22
 
21
23
  def initialize(endpoint = nil, options = {})
@@ -23,8 +25,10 @@ module Faye
23
25
 
24
26
  @endpoint = endpoint || RackAdapter::DEFAULT_ENDPOINT
25
27
  @options = options
28
+ @disabled = []
26
29
 
27
- @transport = Transport.get(self, MANDATORY_CONNECTION_TYPES)
30
+ select_transport(MANDATORY_CONNECTION_TYPES)
31
+
28
32
  @state = UNCONNECTED
29
33
  @channels = Channel::Set.new
30
34
  @message_id = 0
@@ -38,6 +42,10 @@ module Faye
38
42
  }
39
43
  end
40
44
 
45
+ def disable(feature)
46
+ @disabled << feature
47
+ end
48
+
41
49
  def state
42
50
  case @state
43
51
  when UNCONNECTED then :UNCONNECTED
@@ -84,7 +92,9 @@ module Faye
84
92
  if response['successful']
85
93
  @state = CONNECTED
86
94
  @client_id = response['clientId']
87
- @transport = Transport.get(self, response['supportedConnectionTypes'])
95
+
96
+ connection_types = response['supportedConnectionTypes'] - @disabled
97
+ select_transport(connection_types)
88
98
 
89
99
  info('Handshake successful: ?', @client_id)
90
100
 
@@ -152,7 +162,10 @@ module Faye
152
162
  send({
153
163
  'channel' => Channel::DISCONNECT,
154
164
  'clientId' => @client_id
155
- })
165
+
166
+ }) do |response|
167
+ @transport.close if response['successful']
168
+ end
156
169
 
157
170
  info('Clearing channel listeners for ?', @client_id)
158
171
  @channels = Channel::Set.new
@@ -256,6 +269,7 @@ module Faye
256
269
  raise "Cannot publish: '#{channel}' is not a valid channel name"
257
270
  end
258
271
 
272
+ publication = Publication.new
259
273
  connect {
260
274
  info('Client ? queueing published message to ?: ?', @client_id, channel, data)
261
275
 
@@ -263,8 +277,15 @@ module Faye
263
277
  'channel' => channel,
264
278
  'data' => data,
265
279
  'clientId' => @client_id
266
- })
280
+ }) do |response|
281
+ if response['successful']
282
+ publication.set_deferred_status(:succeeded)
283
+ else
284
+ publication.set_deferred_status(:failed, Error.parse(response['error']))
285
+ end
286
+ end
267
287
  }
288
+ publication
268
289
  end
269
290
 
270
291
  def receive_message(message)
@@ -285,6 +306,26 @@ module Faye
285
306
 
286
307
  private
287
308
 
309
+ def select_transport(transport_types)
310
+ Transport.get(self, transport_types) do |transport|
311
+ @transport = transport
312
+
313
+ transport.bind :down do
314
+ if @transport_up.nil? or @transport_up
315
+ @transport_up = false
316
+ trigger('transport:down')
317
+ end
318
+ end
319
+
320
+ transport.bind :up do
321
+ if @transport_up.nil? or not @transport_up
322
+ @transport_up = true
323
+ trigger('transport:up')
324
+ end
325
+ end
326
+ end
327
+ end
328
+
288
329
  def send(message, &callback)
289
330
  message['id'] = generate_message_id
290
331
  @response_callbacks[message['id']] = callback if callback
@@ -0,0 +1,5 @@
1
+ module Faye
2
+ class Publication
3
+ include EventMachine::Deferrable
4
+ end
5
+ end
@@ -3,9 +3,9 @@ module Faye
3
3
 
4
4
  include Logging
5
5
  include Extensible
6
-
7
- META_METHODS = %w[handshake connect disconnect subscribe unsubscribe]
8
6
 
7
+ attr_reader :engine
8
+
9
9
  def initialize(options = {})
10
10
  @options = options || {}
11
11
  engine_opts = @options[:engine] || {}
@@ -91,14 +91,7 @@ module Faye
91
91
 
92
92
  def handle_meta(message, local, &callback)
93
93
  method = Channel.parse(message['channel'])[1]
94
-
95
- unless META_METHODS.include?(method)
96
- response = make_response(message)
97
- response['error'] = Faye::Error.channel_forbidden(message['channel'])
98
- response['successful'] = false
99
- return callback.call([response])
100
- end
101
-
94
+
102
95
  __send__(method, message, local) do |responses|
103
96
  responses = [responses].flatten
104
97
  responses.each(&method(:advize))
@@ -132,15 +125,13 @@ module Faye
132
125
 
133
126
  client_conns = message['supportedConnectionTypes']
134
127
 
135
- unless local
136
- response['supportedConnectionTypes'] = CONNECTION_TYPES
137
-
138
- if client_conns
139
- common_conns = client_conns.select { |c| CONNECTION_TYPES.include?(c) }
140
- response['error'] = Error.conntype_mismatch(*client_conns) if common_conns.empty?
141
- else
142
- response['error'] = Error.parameter_missing('supportedConnectionTypes')
143
- end
128
+ response['supportedConnectionTypes'] = CONNECTION_TYPES
129
+
130
+ if client_conns
131
+ common_conns = client_conns.select { |c| CONNECTION_TYPES.include?(c) }
132
+ response['error'] = Error.conntype_mismatch(*client_conns) if common_conns.empty?
133
+ else
134
+ response['error'] = Error.parameter_missing('supportedConnectionTypes')
144
135
  end
145
136
 
146
137
  response['successful'] = response['error'].nil?
@@ -34,6 +34,7 @@ class Thin::Connection
34
34
  else
35
35
  if @request.parse(data)
36
36
  if @request.websocket?
37
+ @request.env['em.connection'] = self
37
38
  @response.persistent!
38
39
  @response.websocket = true
39
40
  @serving = :websocket
@@ -51,7 +52,6 @@ end
51
52
 
52
53
  class Thin::Request
53
54
  WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
54
-
55
55
  def websocket?
56
56
  @env['HTTP_CONNECTION'] and
57
57
  @env['HTTP_CONNECTION'].split(/\s*,\s*/).include?('Upgrade') and
@@ -1,22 +1,23 @@
1
- require 'em-http'
2
- require 'em-http/version'
3
-
4
1
  module Faye
5
2
 
6
3
  class Transport::Http < Transport
7
- def self.usable?(endpoint)
8
- endpoint.is_a?(String)
4
+ def self.usable?(endpoint, &callback)
5
+ callback.call(endpoint.is_a?(String))
9
6
  end
10
7
 
11
8
  def request(message, timeout)
12
9
  retry_block = retry_block(message, timeout)
13
10
 
11
+ @client.cookies ||= CookieJar::Jar.new
12
+ cookies = @client.cookies.get_cookies(@endpoint)
13
+
14
14
  content = JSON.unparse(message)
15
15
  params = {
16
16
  :head => {
17
+ 'Content-Length' => content.length,
17
18
  'Content-Type' => 'application/json',
18
- 'host' => URI.parse(@endpoint).host,
19
- 'Content-Length' => content.length
19
+ 'Cookie' => cookies * '; ',
20
+ 'Host' => URI.parse(@endpoint).host
20
21
  },
21
22
  :body => content,
22
23
  :timeout => -1 # for em-http-request < 1.0
@@ -31,12 +32,20 @@ module Faye
31
32
  end
32
33
  request.callback do
33
34
  begin
35
+ cookies = [*request.response_header['SET_COOKIE']]
36
+ cookies.each do |cookie|
37
+ @client.cookies.set_cookie(@endpoint, cookie)
38
+ end
34
39
  receive(JSON.parse(request.response))
40
+ trigger(:up)
35
41
  rescue
36
42
  retry_block.call
37
43
  end
38
44
  end
39
- request.errback { retry_block.call }
45
+ request.errback do
46
+ retry_block.call
47
+ trigger(:down)
48
+ end
40
49
  end
41
50
  end
42
51
 
@@ -1,8 +1,8 @@
1
1
  module Faye
2
2
 
3
3
  class Transport::Local < Transport
4
- def self.usable?(endpoint)
5
- endpoint.is_a?(Server)
4
+ def self.usable?(endpoint, &callback)
5
+ callback.call(endpoint.is_a?(Server))
6
6
  end
7
7
 
8
8
  def batching?
@@ -10,7 +10,10 @@ module Faye
10
10
  end
11
11
 
12
12
  def request(message, timeout)
13
- @endpoint.process(message, true) { |responses| receive(responses) }
13
+ message = Faye.copy_object(message)
14
+ @endpoint.process(message, true) do |responses|
15
+ receive(Faye.copy_object(responses))
16
+ end
14
17
  end
15
18
  end
16
19
 
@@ -1,10 +1,8 @@
1
- require 'json'
2
- require 'uri'
3
-
4
1
  module Faye
5
2
  class Transport
6
3
 
7
4
  include Logging
5
+ include Publisher
8
6
  include Timeouts
9
7
 
10
8
  def initialize(client, endpoint)
@@ -18,6 +16,9 @@ module Faye
18
16
  true
19
17
  end
20
18
 
19
+ def close
20
+ end
21
+
21
22
  def connection_type
22
23
  self.class.connection_type
23
24
  end
@@ -68,20 +69,29 @@ module Faye
68
69
  class << self
69
70
  attr_accessor :connection_type
70
71
 
71
- def get(client, connection_types = nil)
72
+ def get(client, connection_types = nil, &callback)
72
73
  endpoint = client.endpoint
73
74
  connection_types ||= supported_connection_types
74
75
 
75
- candidate_class = @transports.find do |(type, klass)|
76
- connection_types.include?(type) and
77
- klass.usable?(endpoint)
76
+ select = lambda do |(conn_type, klass), resume|
77
+ if connection_types.include?(conn_type)
78
+ klass.usable?(endpoint) do |is_usable|
79
+ if is_usable
80
+ callback.call(klass.new(client, endpoint))
81
+ else
82
+ resume.call
83
+ end
84
+ end
85
+ else
86
+ resume.call
87
+ end
78
88
  end
79
89
 
80
- unless candidate_class
90
+ error = lambda do
81
91
  raise "Could not find a usable connection type for #{ endpoint }"
82
92
  end
83
93
 
84
- candidate_class.last.new(client, endpoint)
94
+ Faye.async_each(@transports, select, error)
85
95
  end
86
96
 
87
97
  def register(type, klass)
@@ -94,6 +104,10 @@ module Faye
94
104
  end
95
105
  end
96
106
 
107
+ %w[local web_socket http].each do |type|
108
+ require File.join(ROOT, 'faye', 'transport', type)
109
+ end
110
+
97
111
  end
98
112
  end
99
113
 
@@ -0,0 +1,102 @@
1
+ module Faye
2
+
3
+ class Transport::WebSocket < Transport
4
+ WEBSOCKET_TIMEOUT = 1
5
+
6
+ UNCONNECTED = 1
7
+ CONNECTING = 2
8
+ CONNECTED = 3
9
+
10
+ include EventMachine::Deferrable
11
+
12
+ def self.usable?(endpoint, &callback)
13
+ connected = false
14
+ called = false
15
+ socket_url = endpoint.gsub(/^http(s?):/, 'ws\1:')
16
+ socket = Faye::WebSocket::Client.new(socket_url)
17
+
18
+ socket.onopen = lambda do |event|
19
+ connected = true
20
+ socket.close
21
+ callback.call(true)
22
+ called = true
23
+ socket = nil
24
+ end
25
+
26
+ notconnected = lambda do |*args|
27
+ callback.call(false) unless called or connected
28
+ called = true
29
+ end
30
+
31
+ socket.onclose = socket.onerror = notconnected
32
+ EventMachine.add_timer(WEBSOCKET_TIMEOUT, &notconnected)
33
+ end
34
+
35
+ def batching?
36
+ false
37
+ end
38
+
39
+ def request(messages, timeout = nil)
40
+ @timeout = timeout || @timeout
41
+ @messages ||= {}
42
+ messages.each { |message| @messages[message['id']] = message }
43
+ with_socket { |socket| socket.send(JSON.unparse(messages)) }
44
+ end
45
+
46
+ def with_socket(&resume)
47
+ callback(&resume)
48
+ connect
49
+ end
50
+
51
+ def close
52
+ return if @closed
53
+ @closed = true
54
+ @socket.close if @socket
55
+ end
56
+
57
+ def connect
58
+ return if @closed
59
+
60
+ @state ||= UNCONNECTED
61
+ return unless @state == UNCONNECTED
62
+
63
+ @state = CONNECTING
64
+
65
+ @socket = Faye::WebSocket::Client.new(@endpoint.gsub(/^http(s?):/, 'ws\1:'))
66
+
67
+ @socket.onopen = lambda do |*args|
68
+ @state = CONNECTED
69
+ set_deferred_status(:succeeded, @socket)
70
+ trigger(:up)
71
+ end
72
+
73
+ @socket.onmessage = lambda do |event|
74
+ messages = [JSON.parse(event.data)].flatten
75
+ messages.each { |message| @messages.delete(message['id']) }
76
+ receive(messages)
77
+ end
78
+
79
+ @socket.onclose = lambda do |*args|
80
+ was_connected = (@state == CONNECTED)
81
+ set_deferred_status(:deferred)
82
+ @state = UNCONNECTED
83
+ @socket = nil
84
+
85
+ if was_connected
86
+ resend
87
+ else
88
+ EventMachine.add_timer(@timeout) { connect }
89
+ @timeout = @timeout * 2
90
+ trigger(:down)
91
+ end
92
+ end
93
+ end
94
+
95
+ def resend
96
+ request(@messages.values)
97
+ end
98
+ end
99
+
100
+ Transport.register 'websocket', Transport::WebSocket
101
+
102
+ end