ably 0.1.4 → 0.1.5

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/ably.gemspec +1 -0
  3. data/lib/ably/auth.rb +9 -13
  4. data/lib/ably/models/idiomatic_ruby_wrapper.rb +27 -39
  5. data/lib/ably/modules/conversions.rb +31 -10
  6. data/lib/ably/modules/enum.rb +201 -0
  7. data/lib/ably/modules/event_emitter.rb +81 -0
  8. data/lib/ably/modules/event_machine_helpers.rb +21 -0
  9. data/lib/ably/modules/http_helpers.rb +13 -0
  10. data/lib/ably/modules/state.rb +67 -0
  11. data/lib/ably/realtime.rb +6 -1
  12. data/lib/ably/realtime/channel.rb +117 -56
  13. data/lib/ably/realtime/client.rb +7 -50
  14. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +116 -0
  15. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +63 -0
  16. data/lib/ably/realtime/connection.rb +97 -14
  17. data/lib/ably/realtime/models/error_info.rb +3 -2
  18. data/lib/ably/realtime/models/message.rb +28 -3
  19. data/lib/ably/realtime/models/nil_channel.rb +21 -0
  20. data/lib/ably/realtime/models/protocol_message.rb +35 -27
  21. data/lib/ably/rest/client.rb +39 -23
  22. data/lib/ably/rest/middleware/external_exceptions.rb +1 -1
  23. data/lib/ably/rest/middleware/parse_json.rb +7 -2
  24. data/lib/ably/rest/middleware/parse_message_pack.rb +23 -0
  25. data/lib/ably/rest/models/paged_resource.rb +4 -4
  26. data/lib/ably/util/pub_sub.rb +32 -0
  27. data/lib/ably/version.rb +1 -1
  28. data/spec/acceptance/realtime/channel_spec.rb +1 -0
  29. data/spec/acceptance/realtime/message_spec.rb +136 -0
  30. data/spec/acceptance/rest/base_spec.rb +51 -1
  31. data/spec/acceptance/rest/presence_spec.rb +7 -2
  32. data/spec/integration/modules/state_spec.rb +66 -0
  33. data/spec/{unit → integration/rest}/auth.rb +0 -0
  34. data/spec/support/api_helper.rb +5 -2
  35. data/spec/support/protocol_msgbus_helper.rb +29 -0
  36. data/spec/support/test_app.rb +14 -3
  37. data/spec/unit/{conversions.rb → modules/conversions_spec.rb} +1 -1
  38. data/spec/unit/modules/enum_spec.rb +263 -0
  39. data/spec/unit/modules/event_emitter_spec.rb +81 -0
  40. data/spec/unit/modules/pub_sub_spec.rb +74 -0
  41. data/spec/unit/realtime/channel_spec.rb +27 -0
  42. data/spec/unit/realtime/client_spec.rb +8 -0
  43. data/spec/unit/realtime/connection_spec.rb +40 -0
  44. data/spec/unit/realtime/error_info_spec.rb +9 -1
  45. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
  46. data/spec/unit/realtime/message_spec.rb +2 -2
  47. data/spec/unit/realtime/protocol_message_spec.rb +78 -9
  48. data/spec/unit/rest/{rest_spec.rb → client_spec.rb} +0 -0
  49. data/spec/unit/rest/message_spec.rb +1 -1
  50. metadata +51 -9
  51. data/lib/ably/realtime/callbacks.rb +0 -15
@@ -0,0 +1,63 @@
1
+ module Ably::Realtime
2
+ class Client
3
+ # OutgoingMessageDispatcher is a (private) class that is used to deliver
4
+ # outgoing {Ably::Realtime::Models::ProtocolMessage}s using the {Ably::Realtime::Connection}
5
+ # when the connection state is capable of delivering messages
6
+ class OutgoingMessageDispatcher
7
+ include Ably::Modules::EventMachineHelpers
8
+
9
+ ACTION = Models::ProtocolMessage::ACTION
10
+
11
+ def initialize(client)
12
+ @client = client
13
+ subscribe_to_outgoing_protocol_message_queue
14
+ setup_event_handlers
15
+ end
16
+
17
+ private
18
+ attr_reader :client
19
+
20
+ def connection
21
+ client.connection
22
+ end
23
+
24
+ def can_send_messages?
25
+ connection.connected?
26
+ end
27
+
28
+ def messages_in_outgoing_queue?
29
+ !outgoing_queue.empty?
30
+ end
31
+
32
+ def outgoing_queue
33
+ connection.__outgoing_message_queue__
34
+ end
35
+
36
+ def pending_queue
37
+ connection.__pending_message_queue__
38
+ end
39
+
40
+ def deliver_queued_protocol_messages
41
+ condition = -> { can_send_messages? && messages_in_outgoing_queue? }
42
+ non_blocking_loop_while(condition) do
43
+ protocol_message = outgoing_queue.shift
44
+ pending_queue << protocol_message if protocol_message.ack_required?
45
+ connection.send_text(protocol_message.to_json)
46
+ client.logger.debug("Prot msg sent =>: #{protocol_message.action} #{protocol_message}")
47
+ end
48
+ end
49
+
50
+ def subscribe_to_outgoing_protocol_message_queue
51
+ connection.__outgoing_protocol_msgbus__.subscribe(:message) do |*args|
52
+ deliver_queued_protocol_messages
53
+ end
54
+ end
55
+
56
+ def setup_event_handlers
57
+ connection.on(:connected) do
58
+ deliver_queued_protocol_messages
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,31 +1,85 @@
1
1
  module Ably
2
2
  module Realtime
3
+ # The Connection class represents the connection associated with an Ably Realtime instance.
4
+ # The Connection object exposes the lifecycle and parameters of the realtime connection.
5
+ #
6
+ # Connections will always be in one of the following states:
7
+ #
8
+ # initialized: 0
9
+ # connecting: 1
10
+ # connected: 2
11
+ # disconnected: 3
12
+ # suspended: 4
13
+ # closed: 5
14
+ # failed: 6
15
+ #
16
+ # Note that the states are available as Enum-like constants:
17
+ #
18
+ # Connection::STATE.Initialized
19
+ # Connection::STATE.Connecting
20
+ # Connection::STATE.Connected
21
+ # Connection::STATE.Disconnected
22
+ # Connection::STATE.Suspended
23
+ # Connection::STATE.Closed
24
+ # Connection::STATE.Failed
25
+ #
26
+ # @!attribute [r] state
27
+ # @return {Ably::Realtime::Connection::STATE} connection state
28
+ # @!attribute [r] __outgoing_message_queue__
29
+ # @return [Array] An internal queue used to manage unsent outgoing messages. You should never interface with this array directly.
30
+ # @!attribute [r] __pending_message_queue__
31
+ # @return [Array] An internal queue used to manage sent messages. You should never interface with this array directly.
32
+ #
3
33
  class Connection < EventMachine::Connection
4
34
  include Ably::Modules::Conversions
5
- include Callbacks
35
+ include Ably::Modules::EventEmitter
36
+ extend Ably::Modules::Enum
37
+
38
+ STATE = ruby_enum('STATE',
39
+ :initializing,
40
+ :initialized,
41
+ :connecting,
42
+ :connected,
43
+ :disconnected,
44
+ :suspended,
45
+ :closed,
46
+ :failed
47
+ )
48
+ include Ably::Modules::State
49
+
50
+ attr_reader :__outgoing_message_queue__, :__pending_message_queue__
6
51
 
7
52
  def initialize(client)
8
- @client = client
9
- @message_serial = 0
53
+ @client = client
54
+ @message_serial = 0
55
+ @__outgoing_message_queue__ = []
56
+ @__pending_message_queue__ = []
57
+ @state = STATE.Initializing
10
58
  end
11
59
 
12
- def send(protocol_message)
60
+ # Required for test /unit/realtime/connection_spec.rb
61
+ alias_method :orig_send, :send
62
+
63
+ # Add protocol message to the outgoing message queue and notify the dispatcher that a message is
64
+ # ready to be sent
65
+ def send_protocol_message(protocol_message)
13
66
  add_message_serial_if_ack_required_to(protocol_message) do
14
67
  protocol_message = Models::ProtocolMessage.new(protocol_message)
15
- client.log_http("Prot msg sent =>: #{protocol_message.action_sym} #{protocol_message.to_json}")
16
- driver.text(protocol_message.to_json)
68
+ __outgoing_message_queue__ << protocol_message
69
+ logger.debug("Prot msg queued =>: #{protocol_message.action} #{protocol_message}")
70
+ __outgoing_protocol_msgbus__.publish :message, protocol_message
17
71
  end
18
72
  end
19
73
 
20
74
  # EventMachine::Connection interface
21
75
  def post_init
22
- trigger :initalised
76
+ change_state STATE.Initialized
23
77
 
24
78
  setup_driver
25
79
  end
26
80
 
27
81
  def connection_completed
28
- trigger :connecting
82
+ change_state STATE.Connecting
29
83
 
30
84
  start_tls if client.use_tls?
31
85
  driver.start
@@ -36,7 +90,7 @@ module Ably
36
90
  end
37
91
 
38
92
  def unbind
39
- trigger :disconnected
93
+ change_state STATE.Disconnected
40
94
  end
41
95
 
42
96
  # WebSocket::Driver interface
@@ -50,9 +104,29 @@ module Ably
50
104
  send_data(data)
51
105
  end
52
106
 
107
+ def send_text(text)
108
+ driver.text(text)
109
+ end
110
+
111
+ # Client library internal outgoing message bus
112
+ def __outgoing_protocol_msgbus__
113
+ @__outgoing_protocol_msgbus__ ||= pub_sub_message_bus
114
+ end
115
+
116
+ # Client library internal incoming message bus
117
+ def __incoming_protocol_msgbus__
118
+ @__incoming_protocol_msgbus__ ||= pub_sub_message_bus
119
+ end
120
+
53
121
  private
54
122
  attr_reader :client, :driver, :message_serial
55
123
 
124
+ def pub_sub_message_bus
125
+ Ably::Util::PubSub.new(
126
+ coerce_into: Proc.new { |event| Models::ProtocolMessage::ACTION(event) }
127
+ )
128
+ end
129
+
56
130
  def add_message_serial_if_ack_required_to(protocol_message)
57
131
  if Models::ProtocolMessage.ack_required?(protocol_message[:action])
58
132
  add_message_serial_to(protocol_message) { yield }
@@ -74,16 +148,25 @@ module Ably
74
148
  @driver = WebSocket::Driver.client(self)
75
149
 
76
150
  driver.on("open") do
77
- client.log_http("WebSocket connection opened to #{url}")
78
- trigger :connected
151
+ logger.debug("WebSocket connection opened to #{url}")
152
+ change_state STATE.Connected
79
153
  end
80
154
 
81
155
  driver.on("message") do |event|
82
- message = Models::ProtocolMessage.new(JSON.parse(event.data))
83
- client.log_http("Prot msg recv <=: #{message.action_sym} #{message.to_json}")
84
- client.trigger message.action_sym, message
156
+ begin
157
+ message = Models::ProtocolMessage.new(JSON.parse(event.data).freeze)
158
+ logger.debug("Prot msg recv <=: #{message.action} #{event.data}")
159
+ __incoming_protocol_msgbus__.publish :message, message
160
+ rescue KeyError
161
+ client.logger.error("Unsupported Protocol Message received, unrecognised 'action': #{event.data}\nNo action taken")
162
+ end
85
163
  end
86
164
  end
165
+
166
+ # Used by {Ably::Modules::State} to debug state changes
167
+ def logger
168
+ client.logger
169
+ end
87
170
  end
88
171
  end
89
172
  end
@@ -20,11 +20,12 @@ module Ably::Realtime::Models
20
20
  @json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze)
21
21
  end
22
22
 
23
- %w( message code status ).each do |attribute|
23
+ %w( message code status_code ).each do |attribute|
24
24
  define_method attribute do
25
25
  json[attribute.to_sym]
26
26
  end
27
27
  end
28
+ alias_method :status, :status_code
28
29
 
29
30
  def json
30
31
  @json_object
@@ -32,7 +33,7 @@ module Ably::Realtime::Models
32
33
  alias_method :to_json, :json
33
34
 
34
35
  def to_s
35
- "Error: #{message} (code: #{code}, status: #{status})"
36
+ "Error: #{message} (code: #{code}, status_code: #{status_code})"
36
37
  end
37
38
  end
38
39
  end
@@ -1,4 +1,15 @@
1
1
  module Ably::Realtime::Models
2
+ def self.Message(message, protocol_message = nil)
3
+ case message
4
+ when Ably::Realtime::Models::Message
5
+ message.tap do
6
+ message.assign_to_protocol_message protocol_message
7
+ end
8
+ else
9
+ Message.new(message, protocol_message)
10
+ end
11
+ end
12
+
2
13
  # A class representing an individual message to be sent or received
3
14
  # via the Ably Realtime service.
4
15
  #
@@ -20,11 +31,17 @@ module Ably::Realtime::Models
20
31
  class Message
21
32
  include Shared
22
33
  include Ably::Modules::Conversions
34
+ include EventMachine::Deferrable
23
35
 
24
- def initialize(json_object, protocol_message)
36
+ # {Message} initializer
37
+ #
38
+ # @param json_object [Hash] JSON like object with the underlying message details
39
+ # @param protocol_message [ProtocolMessage] if this message has been published, then it is associated with a {ProtocolMessage}
40
+ #
41
+ def initialize(json_object, protocol_message = nil)
25
42
  @protocol_message = protocol_message
26
43
  @raw_json_object = json_object
27
- @json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze, stop_at: [:data])
44
+ @json_object = IdiomaticRubyWrapper(json_object.clone.freeze, stop_at: [:data])
28
45
  end
29
46
 
30
47
  %w( name client_id ).each do |attribute|
@@ -65,8 +82,16 @@ module Ably::Realtime::Models
65
82
  to_json_object.to_json
66
83
  end
67
84
 
85
+ def assign_to_protocol_message(protocol_message)
86
+ @protocol_message = protocol_message
87
+ end
88
+
68
89
  private
69
- attr_reader :protocol_message
90
+
91
+ def protocol_message
92
+ raise RuntimeError, "Message is not yet published with a ProtocolMessage. ProtocolMessage is nil" if @protocol_message.nil?
93
+ @protocol_message
94
+ end
70
95
 
71
96
  def protocol_message_index
72
97
  protocol_message.messages.index(self)
@@ -0,0 +1,21 @@
1
+ module Ably::Realtime::Models
2
+ # Nil object for Channels, this object is only used within the internal API of this client library
3
+ class NilChannel
4
+ include Ably::Modules::EventEmitter
5
+ extend Ably::Modules::Enum
6
+ STATE = ruby_enum('STATE', Ably::Realtime::Channel::STATE)
7
+ include Ably::Modules::State
8
+
9
+ def initialize
10
+ @state = STATE.Initialized
11
+ end
12
+
13
+ def name
14
+ 'Nil channel'
15
+ end
16
+
17
+ def __incoming_protocol_msgbus__
18
+ @__incoming_protocol_msgbus__ ||= Ably::Util::PubSub.new
19
+ end
20
+ end
21
+ end
@@ -7,9 +7,7 @@ module Ably::Realtime::Models
7
7
  # for further details on the members of a ProtocolMessage
8
8
  #
9
9
  # @!attribute [r] action
10
- # @return [Integer] Protocol Message action from list of {ACTIONS}
11
- # @!attribute [r] action_sym
12
- # @return [Symbol] Protocol Message action as a symbol
10
+ # @return [ACTION] Protocol Message action {Ably::Modules::Enum} from list of {ACTION}. Returns nil if action is unsupported by protocol.
13
11
  # @!attribute [r] count
14
12
  # @return [Integer] The count field is used for ACK and NACK actions. See {http://docs.ably.io/client-lib-development-guide/protocol/#message-acknowledgement message acknowledgement protocol}
15
13
  # @!attribute [r] error_info
@@ -35,13 +33,14 @@ module Ably::Realtime::Models
35
33
  #
36
34
  class ProtocolMessage
37
35
  include Shared
36
+ extend Ably::Modules::Enum
38
37
  include Ably::Modules::Conversions
39
38
 
40
39
  # Actions which are sent by the Ably Realtime API
41
40
  #
42
41
  # The values correspond to the ints which the API
43
42
  # understands.
44
- ACTIONS = {
43
+ ACTION = ruby_enum('ACTION',
45
44
  heartbeat: 0,
46
45
  ack: 1,
47
46
  nack: 2,
@@ -58,23 +57,11 @@ module Ably::Realtime::Models
58
57
  detached: 13,
59
58
  presence: 14,
60
59
  message: 15
61
- }.freeze
62
-
63
- # Retrieve an action symbol by the integer value
64
- def self.action_sym_for(action_int)
65
- @actions_index_by_int ||= ACTIONS.invert.freeze
66
- @actions_index_by_int[action_int]
67
- end
68
-
69
- # Retrive an action integer value from a symbol and raise an exception if invalid
70
- def self.action!(action_sym)
71
- ACTIONS.fetch(action_sym)
72
- end
60
+ )
73
61
 
74
62
  # Indicates this protocol message action will generate an ACK response such as :message or :presence
75
63
  def self.ack_required?(for_action)
76
- for_action = ACTIONS.fetch(for_action) if for_action.kind_of?(Symbol)
77
- [action!(:presence), action!(:message)].include?(for_action)
64
+ [ACTION.Presence, ACTION.Message].include?(ACTION(for_action))
78
65
  end
79
66
 
80
67
  def initialize(json_object)
@@ -82,16 +69,17 @@ module Ably::Realtime::Models
82
69
  @json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze)
83
70
  end
84
71
 
85
- %w( action count
86
- channel channel_serial
72
+ %w( channel channel_serial
87
73
  connection_id connection_serial ).each do |attribute|
88
74
  define_method attribute do
89
75
  json[attribute.to_sym]
90
76
  end
91
77
  end
92
78
 
93
- def action_sym
94
- self.class.action_sym_for(action)
79
+ def action
80
+ ACTION(json[:action])
81
+ rescue KeyError
82
+ raise KeyError, "Action '#{json[:action]}' is not supported by ProtocolMessage"
95
83
  end
96
84
 
97
85
  def error
@@ -103,20 +91,36 @@ module Ably::Realtime::Models
103
91
  end
104
92
 
105
93
  def message_serial
106
- json[:msg_serial]
94
+ Integer(json[:msg_serial])
95
+ rescue TypeError
96
+ raise TypeError, "msg_serial '#{json[:msg_serial]}' is invalid, a positive Integer is expected for a ProtocolMessage"
97
+ end
98
+
99
+ def count
100
+ [1, json[:count].to_i].max
101
+ end
102
+
103
+ def has_message_serial?
104
+ message_serial && true
105
+ rescue TypeError
106
+ false
107
107
  end
108
108
 
109
109
  def messages
110
110
  @messages ||=
111
111
  Array(json[:messages]).map do |message|
112
- Message.new(message, self)
112
+ Ably::Realtime::Models.Message(message, self)
113
113
  end
114
114
  end
115
115
 
116
+ def add_message(message)
117
+ messages << message
118
+ end
119
+
116
120
  def presence
117
121
  @presence ||=
118
122
  Array(json[:presence]).map do |message|
119
- PresenceMessage.new(message, self)
123
+ PresenceMessage(message, self)
120
124
  end
121
125
  end
122
126
 
@@ -131,8 +135,8 @@ module Ably::Realtime::Models
131
135
  end
132
136
 
133
137
  def to_json_object
134
- raise RuntimeError, ":action is missing, cannot generate valid JSON for ProtocolMessage" unless action_sym
135
- raise RuntimeError, ":msg_serial is missing, cannot generate valid JSON for ProtocolMessage" if ack_required? && !message_serial
138
+ raise TypeError, ":action is missing, cannot generate valid JSON for ProtocolMessage" unless action
139
+ raise TypeError, ":msg_serial is missing, cannot generate valid JSON for ProtocolMessage" if ack_required? && !has_message_serial?
136
140
 
137
141
  json.dup.tap do |json_object|
138
142
  json_object[:messages] = messages.map(&:to_json_object) unless messages.empty?
@@ -143,5 +147,9 @@ module Ably::Realtime::Models
143
147
  def to_json(*args)
144
148
  to_json_object.to_json
145
149
  end
150
+
151
+ def to_s
152
+ to_json
153
+ end
146
154
  end
147
155
  end