ably 0.8.3 → 0.8.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -3
  3. data/lib/ably/auth.rb +1 -1
  4. data/lib/ably/exceptions.rb +3 -0
  5. data/lib/ably/models/channel_state_change.rb +41 -0
  6. data/lib/ably/models/connection_state_change.rb +43 -0
  7. data/lib/ably/models/message.rb +1 -1
  8. data/lib/ably/models/presence_message.rb +1 -1
  9. data/lib/ably/models/protocol_message.rb +2 -1
  10. data/lib/ably/modules/state_emitter.rb +4 -1
  11. data/lib/ably/modules/uses_state_machine.rb +28 -4
  12. data/lib/ably/realtime/channel.rb +11 -3
  13. data/lib/ably/realtime/channel/channel_manager.rb +24 -4
  14. data/lib/ably/realtime/channel/channel_state_machine.rb +20 -11
  15. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +4 -4
  16. data/lib/ably/realtime/connection.rb +1 -0
  17. data/lib/ably/realtime/connection/connection_manager.rb +33 -21
  18. data/lib/ably/realtime/connection/connection_state_machine.rb +24 -16
  19. data/lib/ably/rest/channel.rb +3 -2
  20. data/lib/ably/util/crypto.rb +15 -0
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +155 -9
  23. data/spec/acceptance/realtime/client_spec.rb +2 -2
  24. data/spec/acceptance/realtime/connection_failures_spec.rb +8 -4
  25. data/spec/acceptance/realtime/connection_spec.rb +122 -11
  26. data/spec/acceptance/realtime/message_spec.rb +119 -3
  27. data/spec/acceptance/realtime/presence_spec.rb +34 -13
  28. data/spec/acceptance/rest/channel_spec.rb +9 -0
  29. data/spec/acceptance/rest/client_spec.rb +10 -0
  30. data/spec/unit/models/channel_state_change_spec.rb +44 -0
  31. data/spec/unit/models/connection_state_change_spec.rb +54 -0
  32. data/spec/unit/util/crypto_spec.rb +18 -0
  33. metadata +8 -2
@@ -31,8 +31,8 @@ describe Ably::Realtime::Client, :event_machine do
31
31
  it 'fails to connect because a private key cannot be sent over a non-secure connection' do
32
32
  connection.on(:connected) { raise 'Should not have connected' }
33
33
 
34
- connection.on(:failed) do |error|
35
- expect(error).to be_a(Ably::Exceptions::InsecureRequest)
34
+ connection.on(:failed) do |connection_state_change|
35
+ expect(connection_state_change.reason).to be_a(Ably::Exceptions::InsecureRequest)
36
36
  stop_reactor
37
37
  end
38
38
  end
@@ -24,7 +24,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
24
24
  let(:invalid_key) { 'not_an_app.invalid_key_name:invalid_key_value' }
25
25
 
26
26
  it 'enters the failed state and returns a not found error' do
27
- connection.on(:failed) do |error|
27
+ connection.on(:failed) do |connection_state_change|
28
+ error = connection_state_change.reason
28
29
  expect(connection.state).to eq(:failed)
29
30
  # TODO: Check error type is an InvalidToken exception
30
31
  expect(error.status).to eq(404)
@@ -38,7 +39,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
38
39
  let(:invalid_key) { "#{app_id}.invalid_key_name:invalid_key_value" }
39
40
 
40
41
  it 'enters the failed state and returns an authorization error' do
41
- connection.on(:failed) do |error|
42
+ connection.on(:failed) do |connection_state_change|
43
+ error = connection_state_change.reason
42
44
  expect(connection.state).to eq(:failed)
43
45
  # TODO: Check error type is a TokenNotFOund exception
44
46
  expect(error.status).to eq(401)
@@ -182,7 +184,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
182
184
  context '#error_reason' do
183
185
  [:disconnected, :suspended, :failed].each do |state|
184
186
  it "contains the error when state is #{state}" do
185
- connection.on(state) do |error|
187
+ connection.on(state) do |connection_state_change|
188
+ error = connection_state_change.reason
186
189
  expect(connection.error_reason).to eq(error)
187
190
  expect(connection.error_reason.code).to eql(80000)
188
191
  stop_reactor
@@ -488,7 +491,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
488
491
  when_all(*channels.map(&:attach)) do
489
492
  detached_channels = []
490
493
  channels.each do |channel|
491
- channel.on(:detached) do |error|
494
+ channel.on(:detached) do |channel_state_change|
495
+ error = channel_state_change.reason
492
496
  expect(error.message).to match(/Invalid connection key/i)
493
497
  detached_channels << channel
494
498
  next unless detached_channels.count == channel_count
@@ -127,8 +127,8 @@ describe Ably::Realtime::Connection, :event_machine do
127
127
  it 'renews the token on connect, and only makes one subsequent attempt to obtain a new token' do
128
128
  expect(client.rest_client.auth).to receive(:authorise).at_least(:twice).and_call_original
129
129
  connection.once(:disconnected) do
130
- connection.once(:failed) do |error|
131
- expect(error.code).to eql(40140) # token expired
130
+ connection.once(:failed) do |connection_state_change|
131
+ expect(connection_state_change.reason.code).to eql(40140) # token expired
132
132
  stop_reactor
133
133
  end
134
134
  end
@@ -167,8 +167,8 @@ describe Ably::Realtime::Connection, :event_machine do
167
167
 
168
168
  connection.once(:connected) do
169
169
  started_at = Time.now
170
- connection.once(:disconnected) do |error|
171
- expect(error.code).to eq(40140) # Token expired
170
+ connection.once(:disconnected) do |connection_state_change|
171
+ expect(connection_state_change.reason.code).to eq(40140) # Token expired
172
172
 
173
173
  # Token has expired, so now ensure it is not used again
174
174
  stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', original_token_expiry_buffer
@@ -178,7 +178,6 @@ describe Ably::Realtime::Connection, :event_machine do
178
178
  expect(client.auth.current_token_details).to_not be_expired
179
179
  expect(Time.now - started_at >= ttl)
180
180
  expect(original_token).to be_expired
181
- expect(error.code).to eql(40140) # token expired
182
181
  stop_reactor
183
182
  end
184
183
  end
@@ -922,12 +921,12 @@ describe Ably::Realtime::Connection, :event_machine do
922
921
  expect(connection.__outgoing_message_queue__).to be_empty
923
922
  channel.publish 'test'
924
923
 
925
- EventMachine.add_timer(0.02) do
924
+ EventMachine.next_tick do
926
925
  expect(connection.__outgoing_message_queue__).to_not be_empty
927
926
  end
928
927
 
929
928
  connection.once(:connected) do
930
- EventMachine.add_timer(0.02) do
929
+ EventMachine.add_timer(0.1) do
931
930
  expect(connection.__outgoing_message_queue__).to be_empty
932
931
  stop_reactor
933
932
  end
@@ -940,6 +939,8 @@ describe Ably::Realtime::Connection, :event_machine do
940
939
  end
941
940
 
942
941
  context 'when connection enters the :suspended state' do
942
+ let(:client_options) { default_options.merge(:log_level => :fatal) }
943
+
943
944
  before do
944
945
  # Reconfigure client library retry periods so that client stays in suspended state
945
946
  stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
@@ -956,13 +957,21 @@ describe Ably::Realtime::Connection, :event_machine do
956
957
  stop_reactor
957
958
  end
958
959
 
960
+ close_connection_proc = Proc.new do
961
+ EventMachine.add_timer(0.001) do
962
+ if connection.transport.nil?
963
+ close_connection_proc.call
964
+ else
965
+ connection.transport.close_connection_after_writing
966
+ end
967
+ end
968
+ end
969
+
959
970
  # Keep disconnecting the websocket transport after it attempts reconnection
960
- connection.transport.close_connection_after_writing
961
971
  connection.on(:connecting) do
962
- EventMachine.add_timer(0.03) do
963
- connection.transport.close_connection_after_writing
964
- end
972
+ close_connection_proc.call
965
973
  end
974
+ close_connection_proc.call
966
975
  end
967
976
  end
968
977
  end
@@ -979,5 +988,107 @@ describe Ably::Realtime::Connection, :event_machine do
979
988
  end
980
989
  end
981
990
  end
991
+
992
+ context 'connection state change' do
993
+ it 'emits a ConnectionStateChange object' do
994
+ connection.on(:connected) do |connection_state_change|
995
+ expect(connection_state_change).to be_a(Ably::Models::ConnectionStateChange)
996
+ stop_reactor
997
+ end
998
+ end
999
+
1000
+ context 'ConnectionStateChange object' do
1001
+ it 'has current state' do
1002
+ connection.on(:connected) do |connection_state_change|
1003
+ expect(connection_state_change.current).to eq(:connected)
1004
+ stop_reactor
1005
+ end
1006
+ end
1007
+
1008
+ it 'has a previous state' do
1009
+ connection.on(:connected) do |connection_state_change|
1010
+ expect(connection_state_change.previous).to eq(:connecting)
1011
+ stop_reactor
1012
+ end
1013
+ end
1014
+
1015
+ it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
1016
+ connection.on(:connected) do |connection_state_change|
1017
+ expect(connection_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
1018
+ expect(connection_state_change.reason).to be_nil
1019
+ stop_reactor
1020
+ end
1021
+ end
1022
+
1023
+ it 'has an empty reason when there is no error' do
1024
+ connection.on(:closed) do |connection_state_change|
1025
+ expect(connection_state_change.reason).to be_nil
1026
+ stop_reactor
1027
+ end
1028
+ connection.connect do
1029
+ connection.close
1030
+ end
1031
+ end
1032
+
1033
+ context 'on failure' do
1034
+ let(:client_options) { default_options.merge(log_level: :none) }
1035
+
1036
+ it 'has a reason Error object when there is an error on the connection' do
1037
+ connection.on(:failed) do |connection_state_change|
1038
+ expect(connection_state_change.reason).to be_a(Ably::Exceptions::BaseAblyException)
1039
+ stop_reactor
1040
+ end
1041
+ connection.connect do
1042
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
1043
+ client.connection.manager.error_received_from_server error
1044
+ end
1045
+ end
1046
+ end
1047
+
1048
+ context 'retry_in' do
1049
+ let(:client_options) { default_options.merge(log_level: :debug) }
1050
+
1051
+ it 'is nil when a retry is not required' do
1052
+ connection.on(:connected) do |connection_state_change|
1053
+ expect(connection_state_change.retry_in).to be_nil
1054
+ stop_reactor
1055
+ end
1056
+ end
1057
+
1058
+ it 'is 0 when first attempt to connect fails' do
1059
+ connection.once(:connecting) do
1060
+ connection.once(:disconnected) do |connection_state_change|
1061
+ expect(connection_state_change.retry_in).to eql(0)
1062
+ stop_reactor
1063
+ end
1064
+ EventMachine.add_timer(0.001) { connection.transport.unbind }
1065
+ end
1066
+ end
1067
+
1068
+ it 'is 0 when an immediate reconnect will occur' do
1069
+ connection.once(:connected) do
1070
+ connection.once(:disconnected) do |connection_state_change|
1071
+ expect(connection_state_change.retry_in).to eql(0)
1072
+ stop_reactor
1073
+ end
1074
+ connection.transport.unbind
1075
+ end
1076
+ end
1077
+
1078
+ it 'contains the next retry period when an immediate reconnect will not occur' do
1079
+ connection.once(:connected) do
1080
+ connection.once(:connecting) do
1081
+ connection.once(:disconnected) do |connection_state_change|
1082
+ expect(connection_state_change.retry_in).to be > 0
1083
+ stop_reactor
1084
+ end
1085
+ EventMachine.add_timer(0.001) { connection.transport.unbind }
1086
+ end
1087
+ connection.transport.unbind
1088
+ end
1089
+ end
1090
+ end
1091
+ end
1092
+ end
982
1093
  end
983
1094
  end
@@ -181,10 +181,14 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
181
181
 
182
182
  context 'with :echo_messages option set to false' do
183
183
  let(:no_echo_client) do
184
- Ably::Realtime::Client.new(default_options.merge(echo_messages: false))
184
+ Ably::Realtime::Client.new(default_options.merge(echo_messages: false, log_level: :debug))
185
185
  end
186
186
  let(:no_echo_channel) { no_echo_client.channel(channel_name) }
187
187
 
188
+ let(:rest_client) do
189
+ Ably::Rest::Client.new(default_options.merge(log_level: :debug))
190
+ end
191
+
188
192
  it 'will not echo messages to the client but will still broadcast messages to other connected clients', em_timeout: 10 do
189
193
  channel.attach do |echo_channel|
190
194
  no_echo_channel.attach do
@@ -196,13 +200,27 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
196
200
 
197
201
  echo_channel.subscribe('test_event') do |message|
198
202
  expect(message.data).to eql(payload)
199
- EventMachine.add_timer(1) do
203
+ EventMachine.add_timer(1.5) do
200
204
  stop_reactor
201
205
  end
202
206
  end
203
207
  end
204
208
  end
205
209
  end
210
+
211
+ it 'will not echo messages to the client from other REST clients publishing using that connection_ID', em_timeout: 10 do
212
+ skip 'Waiting on realtime#285 to be resolved'
213
+ no_echo_channel.attach do
214
+ no_echo_channel.subscribe('test_event') do |message|
215
+ fail "Message should not have been echoed back"
216
+ end
217
+
218
+ rest_client.channel(channel_name).publish('test_event', nil, connection_id: no_echo_client.connection.id)
219
+ EventMachine.add_timer(1.5) do
220
+ stop_reactor
221
+ end
222
+ end
223
+ end
206
224
  end
207
225
  end
208
226
 
@@ -257,7 +275,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
257
275
 
258
276
  context 'without suitable publishing permissions' do
259
277
  let(:restricted_client) do
260
- Ably::Realtime::Client.new(options.merge(key: restricted_api_key, environment: environment, protocol: protocol))
278
+ Ably::Realtime::Client.new(options.merge(key: restricted_api_key, environment: environment, protocol: protocol, :log_level => :error))
261
279
  end
262
280
  let(:restricted_channel) { restricted_client.channel("cansubscribe:example") }
263
281
  let(:payload) { 'Test message without permission to publish' }
@@ -569,5 +587,103 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
569
587
  end
570
588
  end
571
589
  end
590
+
591
+ describe 'when message is published, the connection disconnects before the ACK is received, and the connection is resumed' do
592
+ let(:event_name) { random_str }
593
+ let(:message_state) { [] }
594
+ let(:connection) { client.connection }
595
+ let(:client_options) { default_options.merge(:log_level => :debug) }
596
+ let(:msgs_received) { [] }
597
+
598
+ it 'publishes the message again, later receives the ACK and only one message is ever received from Ably' do
599
+ on_reconnected = Proc.new do
600
+ expect(message_state).to be_empty
601
+ EventMachine.add_timer(2) do
602
+ expect(message_state).to contain_exactly(:delivered)
603
+ # TODO: Uncomment once issue realtime#42 is resolved
604
+ # expect(msgs_received.length).to eql(1)
605
+ stop_reactor
606
+ end
607
+ end
608
+
609
+ connection.once(:connected) do
610
+ connection.transport.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
611
+ if protocol_message.messages.find { |message| message.name == event_name }
612
+ EventMachine.add_timer(0.001) do
613
+ connection.transport.unbind # trigger failure
614
+ expect(message_state).to be_empty
615
+ connection.once :connected, &on_reconnected
616
+ end
617
+ end
618
+ end
619
+ end
620
+
621
+ channel.publish(event_name).tap do |deferrable|
622
+ deferrable.callback { message_state << :delivered }
623
+ deferrable.errback do
624
+ raise 'Message delivery should not fail'
625
+ end
626
+ end
627
+
628
+ channel.subscribe do |message|
629
+ msgs_received << message
630
+ end
631
+ end
632
+ end
633
+
634
+ describe 'when message is published, the connection disconnects before the ACK is received' do
635
+ let(:connection) { client.connection }
636
+ let(:event_name) { random_str }
637
+
638
+ describe 'the connection becomes suspended' do
639
+ let(:client_options) { default_options.merge(:log_level => :fatal) }
640
+
641
+ it 'calls the errback for all messages' do
642
+ connection.once(:connected) do
643
+ connection.transport.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
644
+ if protocol_message.messages.find { |message| message.name == event_name }
645
+ EventMachine.add_timer(0.001) do
646
+ connection.transition_state_machine :suspended
647
+ end
648
+ end
649
+ end
650
+ end
651
+
652
+ channel.publish(event_name).tap do |deferrable|
653
+ deferrable.callback do
654
+ raise 'Message delivery should not happen'
655
+ end
656
+ deferrable.errback do
657
+ stop_reactor
658
+ end
659
+ end
660
+ end
661
+ end
662
+
663
+ describe 'the connection becomes failed' do
664
+ let(:client_options) { default_options.merge(:log_level => :none) }
665
+
666
+ it 'calls the errback for all messages' do
667
+ connection.once(:connected) do
668
+ connection.transport.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
669
+ if protocol_message.messages.find { |message| message.name == event_name }
670
+ EventMachine.add_timer(0.001) do
671
+ connection.transition_state_machine :failed, reason: RuntimeError.new
672
+ end
673
+ end
674
+ end
675
+ end
676
+
677
+ channel.publish(event_name).tap do |deferrable|
678
+ deferrable.callback do
679
+ raise 'Message delivery should not happen'
680
+ end
681
+ deferrable.errback do
682
+ stop_reactor
683
+ end
684
+ end
685
+ end
686
+ end
687
+ end
572
688
  end
573
689
  end
@@ -40,18 +40,29 @@ describe Ably::Realtime::Presence, :event_machine do
40
40
  end
41
41
 
42
42
  unless expected_state == :left
43
- %w(detached failed).each do |state|
44
- it "raise an exception if the channel is #{state}" do
45
- setup_test(method_name, args, options) do
46
- channel_client_one.attach do
47
- channel_client_one.change_state state.to_sym
48
- expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.#{state}/i
43
+ it 'raise an exception if the channel is detached' do
44
+ setup_test(method_name, args, options) do
45
+ channel_client_one.attach do
46
+ channel_client_one.transition_state_machine :detaching
47
+ channel_client_one.once(:detached) do
48
+ expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.detached/i
49
49
  stop_reactor
50
50
  end
51
51
  end
52
52
  end
53
53
  end
54
54
 
55
+ it 'raise an exception if the channel is failed' do
56
+ setup_test(method_name, args, options) do
57
+ channel_client_one.attach do
58
+ channel_client_one.transition_state_machine :failed
59
+ expect(channel_client_one.state).to eq(:failed)
60
+ expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.failed/i
61
+ stop_reactor
62
+ end
63
+ end
64
+ end
65
+
55
66
  it 'implicitly attaches the channel' do
56
67
  expect(channel_client_one).to_not be_attached
57
68
  presence_client_one.public_send(method_name, args) do
@@ -1061,16 +1072,25 @@ describe Ably::Realtime::Presence, :event_machine do
1061
1072
  presence_client_one.get { raise 'Intentional exception' }
1062
1073
  end
1063
1074
 
1064
- %w(detached failed).each do |state|
1065
- it "raise an exception if the channel is #{state}" do
1066
- channel_client_one.attach do
1067
- channel_client_one.change_state state.to_sym
1068
- expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.#{state}/i
1075
+ it 'raise an exception if the channel is detached' do
1076
+ channel_client_one.attach do
1077
+ channel_client_one.transition_state_machine :detaching
1078
+ channel_client_one.once(:detached) do
1079
+ expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.detached/i
1069
1080
  stop_reactor
1070
1081
  end
1071
1082
  end
1072
1083
  end
1073
1084
 
1085
+ it 'raise an exception if the channel is failed' do
1086
+ channel_client_one.attach do
1087
+ channel_client_one.transition_state_machine :failed
1088
+ expect(channel_client_one.state).to eq(:failed)
1089
+ expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.failed/i
1090
+ stop_reactor
1091
+ end
1092
+ end
1093
+
1074
1094
  context 'during a sync' do
1075
1095
  let(:pages) { 2 }
1076
1096
  let(:members_per_page) { 100 }
@@ -1120,8 +1140,8 @@ describe Ably::Realtime::Presence, :event_machine do
1120
1140
  if protocol_message.action == :sync
1121
1141
  # prevent any more SYNC messages coming through
1122
1142
  client_two.connection.transport.__incoming_protocol_msgbus__.unsubscribe
1123
- channel_client_two.change_state :detaching
1124
- channel_client_two.change_state :detached
1143
+ channel_client_two.transition_state_machine :detaching
1144
+ channel_client_two.transition_state_machine :detached
1125
1145
  end
1126
1146
  end
1127
1147
  end
@@ -1565,6 +1585,7 @@ describe Ably::Realtime::Presence, :event_machine do
1565
1585
  context 'connection failure mid-way through a large member sync' do
1566
1586
  let(:members_count) { 400 }
1567
1587
  let(:sync_pages_received) { [] }
1588
+ let(:client_options) { default_options.merge(log_level: :error) }
1568
1589
 
1569
1590
  it 'resumes the SYNC operation', em_timeout: 15 do
1570
1591
  when_all(*members_count.times.map do |index|