ably 0.7.2 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/LICENSE.txt +1 -1
  2. data/README.md +107 -24
  3. data/SPEC.md +531 -398
  4. data/lib/ably/auth.rb +23 -15
  5. data/lib/ably/exceptions.rb +9 -0
  6. data/lib/ably/models/message.rb +17 -9
  7. data/lib/ably/models/paginated_resource.rb +12 -8
  8. data/lib/ably/models/presence_message.rb +18 -10
  9. data/lib/ably/models/protocol_message.rb +15 -4
  10. data/lib/ably/modules/async_wrapper.rb +4 -3
  11. data/lib/ably/modules/event_emitter.rb +31 -2
  12. data/lib/ably/modules/message_emitter.rb +77 -0
  13. data/lib/ably/modules/safe_deferrable.rb +71 -0
  14. data/lib/ably/modules/safe_yield.rb +41 -0
  15. data/lib/ably/modules/state_emitter.rb +28 -8
  16. data/lib/ably/realtime.rb +0 -5
  17. data/lib/ably/realtime/channel.rb +24 -29
  18. data/lib/ably/realtime/channel/channel_manager.rb +54 -11
  19. data/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  20. data/lib/ably/realtime/client.rb +7 -2
  21. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  22. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  23. data/lib/ably/realtime/connection.rb +41 -9
  24. data/lib/ably/realtime/connection/connection_manager.rb +72 -24
  25. data/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  26. data/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  27. data/lib/ably/realtime/presence.rb +74 -208
  28. data/lib/ably/realtime/presence/members_map.rb +264 -0
  29. data/lib/ably/realtime/presence/presence_manager.rb +59 -0
  30. data/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  31. data/lib/ably/rest/channel.rb +1 -1
  32. data/lib/ably/rest/client.rb +6 -2
  33. data/lib/ably/rest/presence.rb +1 -1
  34. data/lib/ably/util/pub_sub.rb +3 -1
  35. data/lib/ably/util/safe_deferrable.rb +18 -0
  36. data/lib/ably/version.rb +1 -1
  37. data/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  38. data/spec/acceptance/realtime/channel_spec.rb +28 -6
  39. data/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  40. data/spec/acceptance/realtime/connection_spec.rb +55 -10
  41. data/spec/acceptance/realtime/message_spec.rb +32 -0
  42. data/spec/acceptance/realtime/presence_spec.rb +456 -96
  43. data/spec/acceptance/realtime/stats_spec.rb +2 -2
  44. data/spec/acceptance/realtime/time_spec.rb +2 -2
  45. data/spec/acceptance/rest/auth_spec.rb +75 -7
  46. data/spec/shared/client_initializer_behaviour.rb +8 -0
  47. data/spec/shared/safe_deferrable_behaviour.rb +71 -0
  48. data/spec/support/api_helper.rb +1 -1
  49. data/spec/support/event_machine_helper.rb +1 -1
  50. data/spec/support/test_app.rb +13 -7
  51. data/spec/unit/models/message_spec.rb +15 -14
  52. data/spec/unit/models/paginated_resource_spec.rb +4 -4
  53. data/spec/unit/models/presence_message_spec.rb +17 -17
  54. data/spec/unit/models/stat_spec.rb +4 -4
  55. data/spec/unit/modules/async_wrapper_spec.rb +28 -9
  56. data/spec/unit/modules/event_emitter_spec.rb +50 -0
  57. data/spec/unit/modules/state_emitter_spec.rb +76 -2
  58. data/spec/unit/realtime/channel_spec.rb +51 -20
  59. data/spec/unit/realtime/channels_spec.rb +3 -3
  60. data/spec/unit/realtime/connection_spec.rb +30 -0
  61. data/spec/unit/realtime/presence_spec.rb +52 -26
  62. data/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  63. metadata +85 -39
  64. checksums.yaml +0 -7
  65. data/.ruby-version.old +0 -1
data/lib/ably/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = '0.7.2'
2
+ VERSION = '0.7.4'
3
3
  end
@@ -17,10 +17,10 @@ describe Ably::Realtime::Channel, '#history', :event_machine do
17
17
 
18
18
  let(:options) { { :protocol => :json } }
19
19
 
20
- it 'returns a Deferrable' do
20
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
21
21
  channel.publish('event', payload) do |message|
22
22
  history = channel.history
23
- expect(history).to be_a(EventMachine::Deferrable)
23
+ expect(history).to be_a(Ably::Util::SafeDeferrable)
24
24
  history.callback do |messages|
25
25
  expect(messages.count).to eql(1)
26
26
  expect(messages).to be_a(Ably::Models::PaginatedResource)
@@ -84,12 +84,12 @@ describe Ably::Realtime::Channel, :event_machine do
84
84
  end
85
85
  end
86
86
 
87
- it 'returns a Deferrable' do
88
- expect(channel.attach).to be_a(EventMachine::Deferrable)
87
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
88
+ expect(channel.attach).to be_a(Ably::Util::SafeDeferrable)
89
89
  stop_reactor
90
90
  end
91
91
 
92
- it 'calls the Deferrable callback on success' do
92
+ it 'calls the SafeDeferrable callback on success' do
93
93
  channel.attach.callback do |channel|
94
94
  expect(channel).to be_a(Ably::Realtime::Channel)
95
95
  expect(channel.state).to eq(:attached)
@@ -195,6 +195,26 @@ describe Ably::Realtime::Channel, :event_machine do
195
195
  stop_reactor
196
196
  end
197
197
  end
198
+
199
+ context 'and subsequent authorisation with suitable permissions' do
200
+ it 'attaches to the channel successfully and resets the channel error_reason' do
201
+ restricted_channel.attach
202
+ restricted_channel.once(:failed) do
203
+ restricted_client.close do
204
+ # A direct call to #authorise is synchronous
205
+ restricted_client.auth.authorise(api_key: api_key)
206
+
207
+ restricted_client.connect do
208
+ restricted_channel.once(:attached) do
209
+ expect(restricted_channel.error_reason).to be_nil
210
+ stop_reactor
211
+ end
212
+ restricted_channel.attach
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
198
218
  end
199
219
  end
200
220
 
@@ -230,9 +250,11 @@ describe Ably::Realtime::Channel, :event_machine do
230
250
  end
231
251
  end
232
252
 
233
- it 'returns a Deferrable' do
234
- expect(channel.attach).to be_a(EventMachine::Deferrable)
235
- stop_reactor
253
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
254
+ channel.attach do
255
+ expect(channel.detach).to be_a(Ably::Util::SafeDeferrable)
256
+ stop_reactor
257
+ end
236
258
  end
237
259
 
238
260
  it 'calls the Deferrable callback on success' do
@@ -66,7 +66,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
66
66
  )
67
67
  end
68
68
 
69
- let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
69
+ # retry immediately after failure, then one retry every :retry_every_for_tests
70
+ let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
70
71
  let(:state_changes) { Hash.new { |hash, key| hash[key] = 0 } }
71
72
  let(:timer) { Hash.new }
72
73
 
@@ -288,21 +289,76 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
288
289
  end
289
290
 
290
291
  context 'when DISCONNECTED ProtocolMessage received from the server' do
291
- it 'reconnects automatically' do
292
+ it 'reconnects automatically and immediately' do
292
293
  fail_if_suspended_or_failed
293
294
 
294
295
  connection.once(:connected) do
295
296
  connection.once(:disconnected) do
296
- connection.once(:connected) do
297
- state_history = connection.state_history.map { |transition| transition[:state].to_sym }
298
- expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
299
- stop_reactor
297
+ disconnected_at = Time.now.to_f
298
+ connection.once(:connecting) do
299
+ expect(Time.now.to_f).to be_within(0.25).of(disconnected_at)
300
+ connection.once(:connected) do
301
+ state_history = connection.state_history.map { |transition| transition[:state].to_sym }
302
+ expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
303
+ stop_reactor
304
+ end
300
305
  end
301
306
  end
302
307
  protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i)
303
308
  connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
304
309
  end
305
310
  end
311
+
312
+ context 'and subsequently fails to reconnect' do
313
+ let(:retry_every) { 1.5 }
314
+
315
+ before do
316
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
317
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
318
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
319
+ disconnected: { retry_every: retry_every, max_time_in_state: 60 })
320
+ end
321
+
322
+ it "retries every CONNECT_RETRY_CONFIG[:disconnected][:retry_every] seconds" do
323
+ fail_if_suspended_or_failed
324
+
325
+ stubbed_first_attempt = false
326
+
327
+ connection.once(:connected) do
328
+ connection.once(:disconnected) do
329
+ connection.once(:connecting) do
330
+ connection.once(:disconnected) do
331
+ disconnected_at = Time.now.to_f
332
+ connection.once(:connecting) do
333
+ expect(Time.now.to_f - disconnected_at).to be > retry_every
334
+ state_history = connection.state_history.map { |transition| transition[:state].to_sym }
335
+ expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :disconnected, :connecting])
336
+
337
+ # allow one more recoonect when reactor stopped
338
+ expect(connection.manager).to receive(:reconnect_transport)
339
+ stop_reactor
340
+ end
341
+ end
342
+
343
+ # When reconnect called simply open the transport and close immediately
344
+ expect(connection.manager).to receive(:reconnect_transport) do
345
+ next if stubbed_first_attempt
346
+
347
+ connection.manager.setup_transport do
348
+ EventMachine.next_tick do
349
+ connection.transport.unbind
350
+ stubbed_first_attempt = true
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i)
358
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
359
+ end
360
+ end
361
+ end
306
362
  end
307
363
 
308
364
  context 'when websocket transport is closed' do
@@ -359,6 +415,16 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
359
415
  end
360
416
  end
361
417
 
418
+ it 'triggers the resume callback', api_private: true do
419
+ channel.attach do
420
+ connection.transport.close_connection_after_writing
421
+ connection.on_resume do
422
+ expect(connection).to be_connected
423
+ stop_reactor
424
+ end
425
+ end
426
+ end
427
+
362
428
  context 'when messages were published whilst the client was disconnected' do
363
429
  it 'receives the messages published whilst offline' do
364
430
  messages_received = false
@@ -394,55 +460,57 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
394
460
  end
395
461
  end
396
462
 
397
- context 'when failing to resume because the connection_key is not or no longer valid' do
398
- def kill_connection_transport_and_prevent_valid_resume
399
- connection.transport.close_connection_after_writing
400
- connection.configure_new '0123456789abcdef', '0123456789abcdef', -1 # force the resume connection key to be invalid
401
- end
402
-
403
- it 'updates the connection_id and connection_key' do
404
- connection.once(:connected) do
405
- previous_connection_id = connection.id
406
- previous_connection_key = connection.key
463
+ context 'when failing to resume' do
464
+ context 'because the connection_key is not or no longer valid' do
465
+ def kill_connection_transport_and_prevent_valid_resume
466
+ connection.transport.close_connection_after_writing
467
+ connection.configure_new '0123456789abcdef', '0123456789abcdef', -1 # force the resume connection key to be invalid
468
+ end
407
469
 
470
+ it 'updates the connection_id and connection_key' do
408
471
  connection.once(:connected) do
409
- expect(connection.key).to_not eql(previous_connection_key)
410
- expect(connection.id).to_not eql(previous_connection_id)
411
- stop_reactor
412
- end
413
-
414
- kill_connection_transport_and_prevent_valid_resume
415
- end
416
- end
472
+ previous_connection_id = connection.id
473
+ previous_connection_key = connection.key
417
474
 
418
- it 'detaches all channels' do
419
- channel_count = 10
420
- channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
421
- when_all(*channels.map(&:attach)) do
422
- detached_channels = []
423
- channels.each do |channel|
424
- channel.on(:detached) do
425
- detached_channels << channel
426
- next unless detached_channels.count == channel_count
427
- expect(detached_channels.count).to eql(channel_count)
475
+ connection.once(:connected) do
476
+ expect(connection.key).to_not eql(previous_connection_key)
477
+ expect(connection.id).to_not eql(previous_connection_id)
428
478
  stop_reactor
429
479
  end
430
- end
431
480
 
432
- kill_connection_transport_and_prevent_valid_resume
481
+ kill_connection_transport_and_prevent_valid_resume
482
+ end
433
483
  end
434
- end
435
484
 
436
- it 'emits an error on the channel and sets the error reason' do
437
- client.channel(random_str).attach do |channel|
438
- channel.on(:error) do |error|
439
- expect(error.message).to match(/Invalid connection key/i)
440
- expect(error.code).to eql(80008)
441
- expect(channel.error_reason).to eql(error)
442
- stop_reactor
485
+ it 'detaches all channels' do
486
+ channel_count = 10
487
+ channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
488
+ when_all(*channels.map(&:attach)) do
489
+ detached_channels = []
490
+ channels.each do |channel|
491
+ channel.on(:detached) do
492
+ detached_channels << channel
493
+ next unless detached_channels.count == channel_count
494
+ expect(detached_channels.count).to eql(channel_count)
495
+ stop_reactor
496
+ end
497
+ end
498
+
499
+ kill_connection_transport_and_prevent_valid_resume
443
500
  end
501
+ end
444
502
 
445
- kill_connection_transport_and_prevent_valid_resume
503
+ it 'emits an error on the channel and sets the error reason' do
504
+ client.channel(random_str).attach do |channel|
505
+ channel.on(:error) do |error|
506
+ expect(error.message).to match(/Invalid connection key/i)
507
+ expect(error.code).to eql(80008)
508
+ expect(channel.error_reason).to eql(error)
509
+ stop_reactor
510
+ end
511
+
512
+ kill_connection_transport_and_prevent_valid_resume
513
+ end
446
514
  end
447
515
  end
448
516
  end
@@ -461,7 +529,9 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
461
529
  )
462
530
  end
463
531
 
464
- let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
532
+ # Retry immediately and then wait retry_every before every subsequent attempt
533
+ let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
534
+
465
535
  let(:retry_count_for_one_state) { 1 + expected_retry_attempts } # initial connect then disconnected
466
536
  let(:retry_count_for_all_states) { 1 + expected_retry_attempts * 2 } # initial connection, disconnected & then suspended
467
537
 
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'spec_helper'
3
+ require 'ostruct'
3
4
 
4
5
  describe Ably::Realtime::Connection, :event_machine do
5
6
  let(:connection) { client.connection }
@@ -148,14 +149,12 @@ describe Ably::Realtime::Connection, :event_machine do
148
149
  connection.once(:connected) do
149
150
  started_at = Time.now
150
151
  connection.once(:disconnected) do |error|
151
- EventMachine.add_timer(1) do # allow 1 second
152
+ connection.once(:connected) do
153
+ expect(client.auth.current_token).to_not be_expired
152
154
  expect(Time.now - started_at >= ttl)
153
155
  expect(original_token).to be_expired
154
156
  expect(error.code).to eql(40140) # token expired
155
- connection.once(:connected) do
156
- expect(client.auth.current_token).to_not be_expired
157
- stop_reactor
158
- end
157
+ stop_reactor
159
158
  end
160
159
  end
161
160
  end
@@ -234,8 +233,8 @@ describe Ably::Realtime::Connection, :event_machine do
234
233
  end
235
234
 
236
235
  context '#connect' do
237
- it 'returns a Deferrable' do
238
- expect(connection.connect).to be_a(EventMachine::Deferrable)
236
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
237
+ expect(connection.connect).to be_a(Ably::Util::SafeDeferrable)
239
238
  stop_reactor
240
239
  end
241
240
 
@@ -372,9 +371,9 @@ describe Ably::Realtime::Connection, :event_machine do
372
371
  end
373
372
 
374
373
  context '#close' do
375
- it 'returns a Deferrable' do
374
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
376
375
  connection.connect do
377
- expect(connection.close).to be_a(EventMachine::Deferrable)
376
+ expect(connection.close).to be_a(Ably::Util::SafeDeferrable)
378
377
  stop_reactor
379
378
  end
380
379
  end
@@ -504,6 +503,17 @@ describe Ably::Realtime::Connection, :event_machine do
504
503
  stop_reactor
505
504
  end
506
505
  end
506
+
507
+ context 'with a success block that raises an exception' do
508
+ it 'catches the exception and logs the error' do
509
+ connection.on(:connected) do
510
+ expect(connection.logger).to receive(:error).with(/Forced exception/) do
511
+ stop_reactor
512
+ end
513
+ connection.ping { raise 'Forced exception' }
514
+ end
515
+ end
516
+ end
507
517
  end
508
518
 
509
519
  context 'recovery' do
@@ -591,7 +601,7 @@ describe Ably::Realtime::Connection, :event_machine do
591
601
  context 'connection#id and connection#key after recovery' do
592
602
  let(:client_options) { default_options.merge(log_level: :none) }
593
603
 
594
- it 'remain the same' do
604
+ it 'remains the same' do
595
605
  previous_connection_id = nil
596
606
  previous_connection_key = nil
597
607
 
@@ -610,6 +620,22 @@ describe Ably::Realtime::Connection, :event_machine do
610
620
  end
611
621
  end
612
622
  end
623
+
624
+ it 'does not trigger a resume callback', api_private: true do
625
+ connection.once(:connected) do
626
+ connection.transition_state_machine! :failed
627
+ end
628
+
629
+ connection.once(:failed) do
630
+ recover_client = Ably::Realtime::Client.new(default_options.merge(recover: client.connection.recovery_key))
631
+ recover_client.connection.on_resume do
632
+ raise 'Should not trigger resume callback'
633
+ end
634
+ recover_client.connection.on(:connected) do
635
+ EventMachine.add_timer(0.5) { stop_reactor }
636
+ end
637
+ end
638
+ end
613
639
  end
614
640
 
615
641
  context 'when messages have been sent whilst the old connection is disconnected' do
@@ -653,6 +679,7 @@ describe Ably::Realtime::Connection, :event_machine do
653
679
  it 'triggers a fatal error on the connection object, sets the #error_reason and disconnects' do
654
680
  connection.once(:error) do |error|
655
681
  expect(connection.state).to eq(:failed)
682
+ expect(error.message).to match(/Invalid connection key/)
656
683
  expect(connection.error_reason.message).to match(/Invalid connection key/)
657
684
  expect(connection.error_reason.code).to eql(40006)
658
685
  expect(connection.error_reason).to eql(error)
@@ -667,6 +694,7 @@ describe Ably::Realtime::Connection, :event_machine do
667
694
  it 'triggers an error on the connection object, sets the #error_reason, yet will connect anyway' do
668
695
  connection.once(:error) do |error|
669
696
  expect(connection.state).to eq(:connected)
697
+ expect(error.message).to match(/Invalid connection key/i)
670
698
  expect(connection.error_reason.message).to match(/Invalid connection key/i)
671
699
  expect(connection.error_reason.code).to eql(80008)
672
700
  expect(connection.error_reason).to eql(error)
@@ -714,6 +742,23 @@ describe Ably::Realtime::Connection, :event_machine do
714
742
  end
715
743
  end
716
744
 
745
+ context 'protocol failure' do
746
+ let(:client_options) { default_options.merge(protocol: :json) }
747
+
748
+ context 'receiving an invalid ProtocolMessage' do
749
+ it 'emits an error on the connection and logs a fatal error message' do
750
+ connection.connect do
751
+ connection.transport.send(:driver).emit 'message', OpenStruct.new(data: { action: 500 }.to_json)
752
+ end
753
+
754
+ expect(client.logger).to receive(:fatal).with(/Invalid Protocol Message/)
755
+ connection.on(:error) do |error|
756
+ expect(error.message).to match(/Invalid Protocol Message/)
757
+ stop_reactor
758
+ end
759
+ end
760
+ end
761
+ end
717
762
 
718
763
  context 'undocumented method' do
719
764
  context '#internet_up?' do
@@ -195,6 +195,38 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
195
195
  end
196
196
  end
197
197
 
198
+ context 'server incorrectly resends a message that was already received by the client library' do
199
+ let(:messages_received) { [] }
200
+ let(:connection) { client.connection }
201
+ let(:client_options) { default_options.merge(log_level: :fatal) }
202
+
203
+ it 'discards the message and logs it as an error to the channel' do
204
+ first_message_protocol_message = nil
205
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
206
+ first_message_protocol_message ||= protocol_message unless protocol_message.messages.empty?
207
+ end
208
+
209
+ channel.subscribe do |message|
210
+ messages_received << message
211
+ if messages_received.count == 2
212
+ # simulate a duplicate protocol message being received
213
+ EventMachine.next_tick do
214
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, first_message_protocol_message
215
+ end
216
+ end
217
+ end
218
+ 2.times { |i| EventMachine.add_timer(i.to_f / 5) { channel.publish('event', 'data') } }
219
+
220
+ channel.on(:error) do |error|
221
+ expect(error.message).to match(/duplicate/)
222
+ EventMachine.add_timer(0.5) do
223
+ expect(messages_received.count).to eql(2)
224
+ stop_reactor
225
+ end
226
+ end
227
+ end
228
+ end
229
+
198
230
  context 'encoding and decoding encrypted messages' do
199
231
  shared_examples 'an Ably encrypter and decrypter' do |item, data|
200
232
  let(:algorithm) { data['algorithm'].upcase }