faye-huboard 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,334 @@
1
+ module Faye
2
+ class Client
3
+
4
+ include 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
+
20
+ extend Forwardable
21
+ def_delegators :@dispatcher, :disable, :set_header
22
+
23
+ def initialize(endpoint = nil, options = {})
24
+ super()
25
+ info('New client created for ?', endpoint)
26
+
27
+ @endpoint = endpoint || RackAdapter::DEFAULT_ENDPOINT
28
+ @channels = Channel::Set.new
29
+ @dispatcher = Dispatcher.new(self, @endpoint, options)
30
+
31
+ @message_id = 0
32
+ @state = UNCONNECTED
33
+
34
+ @response_callbacks = {}
35
+
36
+ @advice = {
37
+ 'reconnect' => RETRY,
38
+ 'interval' => 1000.0 * (options[:interval] || Engine::INTERVAL),
39
+ 'timeout' => 1000.0 * (options[:timeout] || CONNECTION_TIMEOUT)
40
+ }
41
+ @dispatcher.timeout = @advice['timeout'] / 1000.0
42
+
43
+ @dispatcher.bind(:message, &method(:receive_message))
44
+ end
45
+
46
+ # Request
47
+ # MUST include: * channel
48
+ # * version
49
+ # * supportedConnectionTypes
50
+ # MAY include: * minimumVersion
51
+ # * ext
52
+ # * id
53
+ #
54
+ # Success Response Failed Response
55
+ # MUST include: * channel MUST include: * channel
56
+ # * version * successful
57
+ # * supportedConnectionTypes * error
58
+ # * clientId MAY include: * supportedConnectionTypes
59
+ # * successful * advice
60
+ # MAY include: * minimumVersion * version
61
+ # * advice * minimumVersion
62
+ # * ext * ext
63
+ # * id * id
64
+ # * authSuccessful
65
+ def handshake(&block)
66
+ return if @advice['reconnect'] == NONE
67
+ return if @state != UNCONNECTED
68
+
69
+ @state = CONNECTING
70
+
71
+ info('Initiating handshake with ?', @endpoint)
72
+ @dispatcher.select_transport(MANDATORY_CONNECTION_TYPES)
73
+
74
+ send_message({
75
+ 'channel' => Channel::HANDSHAKE,
76
+ 'version' => BAYEUX_VERSION,
77
+ 'supportedConnectionTypes' => [@dispatcher.connection_type]
78
+
79
+ }, {}) do |response|
80
+
81
+ if response['successful']
82
+ @state = CONNECTED
83
+ @dispatcher.client_id = response['clientId']
84
+
85
+ @dispatcher.select_transport(response['supportedConnectionTypes'])
86
+
87
+ info('Handshake successful: ?', @dispatcher.client_id)
88
+
89
+ subscribe(@channels.keys, true)
90
+ block.call if block_given?
91
+
92
+ else
93
+ info('Handshake unsuccessful')
94
+ EventMachine.add_timer(@dispatcher.retry) { handshake(&block) }
95
+ @state = UNCONNECTED
96
+ end
97
+ end
98
+ end
99
+
100
+ # Request Response
101
+ # MUST include: * channel MUST include: * channel
102
+ # * clientId * successful
103
+ # * connectionType * clientId
104
+ # MAY include: * ext MAY include: * error
105
+ # * id * advice
106
+ # * ext
107
+ # * id
108
+ # * timestamp
109
+ def connect(&block)
110
+ return if @advice['reconnect'] == NONE or
111
+ @state == DISCONNECTED
112
+
113
+ return handshake { connect(&block) } if @state == UNCONNECTED
114
+
115
+ callback(&block)
116
+ return unless @state == CONNECTED
117
+
118
+ info('Calling deferred actions for ?', @dispatcher.client_id)
119
+ set_deferred_status(:succeeded)
120
+ set_deferred_status(:unknown)
121
+
122
+ return unless @connect_request.nil?
123
+ @connect_request = true
124
+
125
+ info('Initiating connection for ?', @dispatcher.client_id)
126
+
127
+ send_message({
128
+ 'channel' => Channel::CONNECT,
129
+ 'clientId' => @dispatcher.client_id,
130
+ 'connectionType' => @dispatcher.connection_type
131
+
132
+ }, {}) do
133
+ cycle_connection
134
+ end
135
+ end
136
+
137
+ # Request Response
138
+ # MUST include: * channel MUST include: * channel
139
+ # * clientId * successful
140
+ # MAY include: * ext * clientId
141
+ # * id MAY include: * error
142
+ # * ext
143
+ # * id
144
+ def disconnect
145
+ return unless @state == CONNECTED
146
+ @state = DISCONNECTED
147
+
148
+ info('Disconnecting ?', @dispatcher.client_id)
149
+
150
+ send_message({
151
+ 'channel' => Channel::DISCONNECT,
152
+ 'clientId' => @dispatcher.client_id
153
+
154
+ }, {}) do |response|
155
+ @dispatcher.close if response['successful']
156
+ end
157
+
158
+ info('Clearing channel listeners for ?', @dispatcher.client_id)
159
+ @channels = Channel::Set.new
160
+ end
161
+
162
+ # Request Response
163
+ # MUST include: * channel MUST include: * channel
164
+ # * clientId * successful
165
+ # * subscription * clientId
166
+ # MAY include: * ext * subscription
167
+ # * id MAY include: * error
168
+ # * advice
169
+ # * ext
170
+ # * id
171
+ # * timestamp
172
+ def subscribe(channel, force = false, &block)
173
+ if Array === channel
174
+ return channel.map { |c| subscribe(c, force, &block) }
175
+ end
176
+
177
+ subscription = Subscription.new(self, channel, block)
178
+ has_subscribe = @channels.has_subscription?(channel)
179
+
180
+ if has_subscribe and not force
181
+ @channels.subscribe([channel], block)
182
+ subscription.set_deferred_status(:succeeded)
183
+ return subscription
184
+ end
185
+
186
+ connect {
187
+ info('Client ? attempting to subscribe to ?', @dispatcher.client_id, channel)
188
+ @channels.subscribe([channel], block) unless force
189
+
190
+ send_message({
191
+ 'channel' => Channel::SUBSCRIBE,
192
+ 'clientId' => @dispatcher.client_id,
193
+ 'subscription' => channel
194
+
195
+ }, {}) do |response|
196
+ unless response['successful']
197
+ subscription.set_deferred_status(:failed, Error.parse(response['error']))
198
+ next @channels.unsubscribe(channel, block)
199
+ end
200
+
201
+ channels = [response['subscription']].flatten
202
+ info('Subscription acknowledged for ? to ?', @dispatcher.client_id, channels)
203
+ subscription.set_deferred_status(:succeeded)
204
+ end
205
+ }
206
+ subscription
207
+ end
208
+
209
+ # Request Response
210
+ # MUST include: * channel MUST include: * channel
211
+ # * clientId * successful
212
+ # * subscription * clientId
213
+ # MAY include: * ext * subscription
214
+ # * id MAY include: * error
215
+ # * advice
216
+ # * ext
217
+ # * id
218
+ # * timestamp
219
+ def unsubscribe(channel, &block)
220
+ if Array === channel
221
+ return channel.map { |c| unsubscribe(c, &block) }
222
+ end
223
+
224
+ dead = @channels.unsubscribe(channel, block)
225
+ return unless dead
226
+
227
+ connect {
228
+ info('Client ? attempting to unsubscribe from ?', @dispatcher.client_id, channel)
229
+
230
+ send_message({
231
+ 'channel' => Channel::UNSUBSCRIBE,
232
+ 'clientId' => @dispatcher.client_id,
233
+ 'subscription' => channel
234
+
235
+ }, {}) do |response|
236
+ next unless response['successful']
237
+
238
+ channels = [response['subscription']].flatten
239
+ info('Unsubscription acknowledged for ? from ?', @dispatcher.client_id, channels)
240
+ end
241
+ }
242
+ end
243
+
244
+ # Request Response
245
+ # MUST include: * channel MUST include: * channel
246
+ # * data * successful
247
+ # MAY include: * clientId MAY include: * id
248
+ # * id * error
249
+ # * ext * ext
250
+ def publish(channel, data, options = {})
251
+ publication = Publication.new
252
+ connect {
253
+ info('Client ? queueing published message to ?: ?', @dispatcher.client_id, channel, data)
254
+
255
+ send_message({
256
+ 'channel' => channel,
257
+ 'data' => data,
258
+ 'clientId' => @dispatcher.client_id
259
+
260
+ }, options) do |response|
261
+ if response['successful']
262
+ publication.set_deferred_status(:succeeded)
263
+ else
264
+ publication.set_deferred_status(:failed, Error.parse(response['error']))
265
+ end
266
+ end
267
+ }
268
+ publication
269
+ end
270
+
271
+ private
272
+
273
+ def send_message(message, options, &callback)
274
+ message['id'] = generate_message_id
275
+
276
+ timeout = [nil, 0].include?(@advice['timeout']) ?
277
+ 1.2 * @dispatcher.retry :
278
+ 1.2 * @advice['timeout'] / 1000.0
279
+
280
+ pipe_through_extensions(:outgoing, message, nil) do |message|
281
+ next unless message
282
+ @response_callbacks[message['id']] = callback if callback
283
+ @dispatcher.send_message(message, timeout, options)
284
+ end
285
+ end
286
+
287
+ def generate_message_id
288
+ @message_id += 1
289
+ @message_id = 0 if @message_id >= 2**32
290
+ @message_id.to_s(36)
291
+ end
292
+
293
+ def receive_message(message)
294
+ id = message['id']
295
+
296
+ if message.has_key?('successful')
297
+ callback = @response_callbacks.delete(id)
298
+ end
299
+
300
+ pipe_through_extensions(:incoming, message, nil) do |message|
301
+ next unless message
302
+ handle_advice(message['advice']) if message['advice']
303
+ deliver_message(message)
304
+ callback.call(message) if callback
305
+ end
306
+ end
307
+
308
+ def handle_advice(advice)
309
+ @advice.update(advice)
310
+ @dispatcher.timeout = @advice['timeout'] / 1000.0
311
+
312
+ if @advice['reconnect'] == HANDSHAKE and @state != DISCONNECTED
313
+ @state = UNCONNECTED
314
+ @dispatcher.client_id = nil
315
+ cycle_connection
316
+ end
317
+ end
318
+
319
+ def deliver_message(message)
320
+ return unless message.has_key?('channel') and message.has_key?('data')
321
+ info('Client ? calling listeners for ? with ?', @dispatcher.client_id, message['channel'], message['data'])
322
+ @channels.distribute_message(message)
323
+ end
324
+
325
+ def cycle_connection
326
+ if @connect_request
327
+ @connect_request = nil
328
+ info('Closed connection for ?', @dispatcher.client_id)
329
+ end
330
+ EventMachine.add_timer(@advice['interval'] / 1000.0) { connect }
331
+ end
332
+
333
+ end
334
+ end
@@ -0,0 +1,146 @@
1
+ module Faye
2
+ class Dispatcher
3
+
4
+ class Envelope < Struct.new(:message, :timeout, :attempts, :deadline, :request, :timer)
5
+ end
6
+
7
+ MAX_REQUEST_SIZE = 2048
8
+ DEFAULT_RETRY = 5.0
9
+
10
+ UP = 1
11
+ DOWN = 2
12
+
13
+ include Publisher
14
+ include Logging
15
+ extend Forwardable
16
+ def_delegators :@transport, :connection_type
17
+
18
+ attr_accessor :client_id, :timeout
19
+ attr_reader :cookies, :endpoint, :headers, :max_request_size, :retry, :transports
20
+
21
+ def initialize(client, endpoint, options)
22
+ super()
23
+
24
+ @client = client
25
+ @endpoint = Faye.parse_url(endpoint)
26
+ @alternates = options[:endpoints] || {}
27
+
28
+ @cookies = CookieJar::Jar.new
29
+ @disabled = []
30
+ @envelopes = {}
31
+ @headers = {}
32
+ @retry = options[:retry] || DEFAULT_RETRY
33
+ @state = 0
34
+ @transports = {}
35
+
36
+ @alternates.each do |type, url|
37
+ @alternates[type] = Faye.parse_url(url)
38
+ end
39
+
40
+ @max_request_size = MAX_REQUEST_SIZE
41
+ end
42
+
43
+ def endpoint_for(connection_type)
44
+ @alternates[connection_type] || @endpoint
45
+ end
46
+
47
+ def disable(feature)
48
+ @disabled << feature
49
+ end
50
+
51
+ def set_header(name, value)
52
+ @headers[name.to_s] = value.to_s
53
+ end
54
+
55
+ def close
56
+ transport = @transport
57
+ @transport = nil
58
+ transport.close if transport
59
+ end
60
+
61
+ def select_transport(transport_types)
62
+ Transport.get(self, transport_types, @disabled) do |transport|
63
+ debug('Selected ? transport for ?', transport.connection_type, transport.endpoint)
64
+
65
+ next if transport == @transport
66
+ @transport.close if @transport
67
+
68
+ @transport = transport
69
+ end
70
+ end
71
+
72
+ def send_message(message, timeout, options = {})
73
+ return unless @transport
74
+
75
+ id = message['id']
76
+ attempts = options[:attempts]
77
+ deadline = options[:deadline] && Time.now.to_f + options[:deadline]
78
+ envelope = @envelopes[id] ||= Envelope.new(message, timeout, attempts, deadline, nil, nil)
79
+
80
+ return if envelope.request or envelope.timer
81
+
82
+ if attempts_exhausted(envelope) or deadline_passed(envelope)
83
+ @envelopes.delete(id)
84
+ return
85
+ end
86
+
87
+ envelope.timer = EventMachine.add_timer(timeout) do
88
+ handle_error(message)
89
+ end
90
+
91
+ envelope.request = @transport.send_message(message)
92
+ end
93
+
94
+ def handle_response(reply)
95
+ if reply.has_key?('successful') and envelope = @envelopes.delete(reply['id'])
96
+ EventMachine.cancel_timer(envelope.timer) if envelope.timer
97
+ end
98
+
99
+ trigger(:message, reply)
100
+
101
+ return if @state == UP
102
+ @state = UP
103
+ @client.trigger('transport:up')
104
+ end
105
+
106
+ def handle_error(message, immediate = false)
107
+ return unless envelope = @envelopes[message['id']]
108
+ return unless request = envelope.request
109
+
110
+ request.callback do |req|
111
+ req.close if req.respond_to?(:close)
112
+ end
113
+
114
+ EventMachine.cancel_timer(envelope.timer)
115
+ envelope.request = envelope.timer = nil
116
+
117
+ if immediate
118
+ send_message(envelope.message, envelope.timeout)
119
+ else
120
+ envelope.timer = EventMachine.add_timer(@retry) do
121
+ envelope.timer = nil
122
+ send_message(envelope.message, envelope.timeout)
123
+ end
124
+ end
125
+
126
+ return if @state == DOWN
127
+ @state = DOWN
128
+ @client.trigger('transport:down')
129
+ end
130
+
131
+ private
132
+
133
+ def attempts_exhausted(envelope)
134
+ return false unless envelope.attempts
135
+ envelope.attempts -= 1
136
+ return false if envelope.attempts >= 0
137
+ return true
138
+ end
139
+
140
+ def deadline_passed(envelope)
141
+ return false unless deadline = envelope.deadline
142
+ return false if Time.now.to_f <= deadline
143
+ return true
144
+ end
145
+ end
146
+ end