ably-rest 0.7.1 → 0.7.3

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 (148) hide show
  1. checksums.yaml +13 -5
  2. data/.gitmodules +1 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -3
  5. data/SPEC.md +495 -419
  6. data/ably-rest.gemspec +19 -5
  7. data/lib/ably-rest.rb +9 -1
  8. data/lib/submodules/ably-ruby/.gitignore +6 -0
  9. data/lib/submodules/ably-ruby/.rspec +1 -0
  10. data/lib/submodules/ably-ruby/.ruby-version.old +1 -0
  11. data/lib/submodules/ably-ruby/.travis.yml +10 -0
  12. data/lib/submodules/ably-ruby/Gemfile +4 -0
  13. data/lib/submodules/ably-ruby/LICENSE.txt +22 -0
  14. data/lib/submodules/ably-ruby/README.md +122 -0
  15. data/lib/submodules/ably-ruby/Rakefile +34 -0
  16. data/lib/submodules/ably-ruby/SPEC.md +1794 -0
  17. data/lib/submodules/ably-ruby/ably.gemspec +36 -0
  18. data/lib/submodules/ably-ruby/lib/ably.rb +12 -0
  19. data/lib/submodules/ably-ruby/lib/ably/auth.rb +438 -0
  20. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +69 -0
  21. data/lib/submodules/ably-ruby/lib/ably/logger.rb +102 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +37 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +223 -0
  24. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +132 -0
  25. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +108 -0
  26. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base64.rb +40 -0
  27. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/cipher.rb +83 -0
  28. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/json.rb +34 -0
  29. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/utf8.rb +26 -0
  30. data/lib/submodules/ably-ruby/lib/ably/models/nil_logger.rb +20 -0
  31. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +173 -0
  32. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +147 -0
  33. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +210 -0
  34. data/lib/submodules/ably-ruby/lib/ably/models/stat.rb +161 -0
  35. data/lib/submodules/ably-ruby/lib/ably/models/token.rb +74 -0
  36. data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +15 -0
  37. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +62 -0
  38. data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +69 -0
  39. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +100 -0
  40. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +69 -0
  41. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +202 -0
  42. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +128 -0
  43. data/lib/submodules/ably-ruby/lib/ably/modules/event_machine_helpers.rb +26 -0
  44. data/lib/submodules/ably-ruby/lib/ably/modules/http_helpers.rb +41 -0
  45. data/lib/submodules/ably-ruby/lib/ably/modules/message_pack.rb +14 -0
  46. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +41 -0
  47. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +153 -0
  48. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +57 -0
  49. data/lib/submodules/ably-ruby/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  50. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +74 -0
  51. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +64 -0
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +298 -0
  53. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +92 -0
  54. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +69 -0
  55. data/lib/submodules/ably-ruby/lib/ably/realtime/channels.rb +50 -0
  56. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +184 -0
  57. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +184 -0
  58. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +70 -0
  59. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +445 -0
  60. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +368 -0
  61. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +91 -0
  62. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +188 -0
  63. data/lib/submodules/ably-ruby/lib/ably/realtime/models/nil_channel.rb +30 -0
  64. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +564 -0
  65. data/lib/submodules/ably-ruby/lib/ably/rest.rb +43 -0
  66. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +104 -0
  67. data/lib/submodules/ably-ruby/lib/ably/rest/channels.rb +44 -0
  68. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +396 -0
  69. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/encoder.rb +49 -0
  70. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +41 -0
  71. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  72. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
  73. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +58 -0
  74. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_json.rb +27 -0
  75. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +27 -0
  76. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +92 -0
  77. data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +105 -0
  78. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +43 -0
  79. data/lib/submodules/ably-ruby/lib/ably/version.rb +3 -0
  80. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +154 -0
  81. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +558 -0
  82. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +119 -0
  83. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +575 -0
  84. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +785 -0
  85. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +457 -0
  86. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +55 -0
  87. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1001 -0
  88. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +23 -0
  89. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +27 -0
  90. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +564 -0
  91. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +165 -0
  92. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +134 -0
  93. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +41 -0
  94. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +273 -0
  95. data/lib/submodules/ably-ruby/spec/acceptance/rest/encoders_spec.rb +185 -0
  96. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +247 -0
  97. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +292 -0
  98. data/lib/submodules/ably-ruby/spec/acceptance/rest/stats_spec.rb +172 -0
  99. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +15 -0
  100. data/lib/submodules/ably-ruby/spec/resources/crypto-data-128.json +56 -0
  101. data/lib/submodules/ably-ruby/spec/resources/crypto-data-256.json +56 -0
  102. data/lib/submodules/ably-ruby/spec/rspec_config.rb +57 -0
  103. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +212 -0
  104. data/lib/submodules/ably-ruby/spec/shared/model_behaviour.rb +86 -0
  105. data/lib/submodules/ably-ruby/spec/shared/protocol_msgbus_behaviour.rb +36 -0
  106. data/lib/submodules/ably-ruby/spec/spec_helper.rb +20 -0
  107. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +60 -0
  108. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +104 -0
  109. data/lib/submodules/ably-ruby/spec/support/markdown_spec_formatter.rb +118 -0
  110. data/lib/submodules/ably-ruby/spec/support/private_api_formatter.rb +36 -0
  111. data/lib/submodules/ably-ruby/spec/support/protocol_helper.rb +32 -0
  112. data/lib/submodules/ably-ruby/spec/support/random_helper.rb +15 -0
  113. data/lib/submodules/ably-ruby/spec/support/rest_testapp_before_retry.rb +15 -0
  114. data/lib/submodules/ably-ruby/spec/support/test_app.rb +113 -0
  115. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +68 -0
  116. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +146 -0
  117. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +18 -0
  118. data/lib/submodules/ably-ruby/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +349 -0
  119. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  120. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  121. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/json_spec.rb +135 -0
  122. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/utf8_spec.rb +56 -0
  123. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +389 -0
  124. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +288 -0
  125. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +386 -0
  126. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +315 -0
  127. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +113 -0
  128. data/lib/submodules/ably-ruby/spec/unit/models/token_spec.rb +86 -0
  129. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +124 -0
  130. data/lib/submodules/ably-ruby/spec/unit/modules/conversions_spec.rb +72 -0
  131. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +272 -0
  132. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +184 -0
  133. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +283 -0
  134. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +206 -0
  135. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +81 -0
  136. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +30 -0
  137. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +33 -0
  138. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
  139. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +111 -0
  140. data/lib/submodules/ably-ruby/spec/unit/realtime/realtime_spec.rb +9 -0
  141. data/lib/submodules/ably-ruby/spec/unit/realtime/websocket_transport_spec.rb +25 -0
  142. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +109 -0
  143. data/lib/submodules/ably-ruby/spec/unit/rest/channels_spec.rb +79 -0
  144. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +53 -0
  145. data/lib/submodules/ably-ruby/spec/unit/rest/rest_spec.rb +10 -0
  146. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +87 -0
  147. data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +86 -0
  148. metadata +182 -27
@@ -0,0 +1,188 @@
1
+ module Ably::Realtime
2
+ class Connection
3
+ # EventMachine WebSocket transport
4
+ # @api private
5
+ class WebsocketTransport < EventMachine::Connection
6
+ include Ably::Modules::EventEmitter
7
+ extend Ably::Modules::Enum
8
+
9
+ # Valid WebSocket connection states
10
+ STATE = ruby_enum('STATE',
11
+ :initialized,
12
+ :connecting,
13
+ :connected,
14
+ :disconnecting,
15
+ :disconnected
16
+ )
17
+ include Ably::Modules::StateEmitter
18
+
19
+ def initialize(connection, url)
20
+ @connection = connection
21
+ @state = STATE.Initialized
22
+ @url = url
23
+
24
+ setup_event_handlers
25
+ end
26
+
27
+ # Disconnect the socket transport connection and write all pending text.
28
+ # If Disconnected state is not automatically triggered, it will be triggered automatically
29
+ # @return [void]
30
+ # @api public
31
+ def disconnect
32
+ close_connection_after_writing
33
+ change_state STATE.Disconnecting
34
+ create_timer(2) do
35
+ # if connection is not disconnected within 2s, set state as disconnected
36
+ change_state STATE.Disconnected
37
+ end
38
+ end
39
+
40
+ # Network connection has been established
41
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
42
+ def post_init
43
+ clear_timer
44
+ change_state STATE.Connecting
45
+ setup_driver
46
+ end
47
+
48
+ # Remote TCP connection attempt completes successfully
49
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
50
+ def connection_completed
51
+ change_state STATE.Connected
52
+ start_tls if client.use_tls?
53
+ driver.start
54
+ end
55
+
56
+ # Called by the event loop whenever data has been received by the network connection.
57
+ # Simply pass onto the WebSocket driver to process and determine content boundaries.
58
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
59
+ def receive_data(data)
60
+ driver.parse(data)
61
+ end
62
+
63
+ # Called whenever a connection (either a server or client connection) is closed
64
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
65
+ def unbind
66
+ change_state STATE.Disconnected
67
+ end
68
+
69
+ # URL end point including initialization configuration
70
+ # {http://www.rubydoc.info/gems/websocket-driver/0.3.5/WebSocket/Driver WebSocket::Driver} interface
71
+ def url
72
+ @url
73
+ end
74
+
75
+ # {http://www.rubydoc.info/gems/websocket-driver/0.3.5/WebSocket/Driver WebSocket::Driver} interface
76
+ def write(data)
77
+ send_data(data)
78
+ end
79
+
80
+ # True if socket connection is ready to be released
81
+ # i.e. it is not currently connecting or connected
82
+ def ready_for_release?
83
+ !connecting? && !connected?
84
+ end
85
+
86
+ # @!attribute [r] __incoming_protocol_msgbus__
87
+ # @return [Ably::Util::PubSub] Websocket Transport internal incoming protocol message bus
88
+ # @api private
89
+ def __incoming_protocol_msgbus__
90
+ @__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus
91
+ end
92
+
93
+ # @!attribute [r] __outgoing_protocol_msgbus__
94
+ # @return [Ably::Util::PubSub] Websocket Transport internal outgoing protocol message bus
95
+ # @api private
96
+ def __outgoing_protocol_msgbus__
97
+ @__outgoing_protocol_msgbus__ ||= create_pub_sub_message_bus
98
+ end
99
+
100
+ private
101
+ attr_reader :connection, :driver
102
+
103
+ # Send object down the WebSocket driver connection as a serialized string/byte array based on protocol
104
+ # @param [Object] object to serialize and send to the WebSocket driver
105
+ # @api public
106
+ def send_object(object)
107
+ case client.protocol
108
+ when :json
109
+ driver.text(object.to_json)
110
+ when :msgpack
111
+ driver.binary(object.to_msgpack.unpack('C*'))
112
+ else
113
+ client.logger.fatal "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket"
114
+ end
115
+ end
116
+
117
+ def setup_event_handlers
118
+ __outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
119
+ send_object protocol_message
120
+ client.logger.debug "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}"
121
+ end
122
+ end
123
+
124
+ def clear_timer
125
+ if @timer
126
+ @timer.cancel
127
+ @timer = nil
128
+ end
129
+ end
130
+
131
+ def create_timer(period)
132
+ @timer = EventMachine::Timer.new(period) do
133
+ yield
134
+ end
135
+ end
136
+
137
+ def setup_driver
138
+ @driver = WebSocket::Driver.client(self)
139
+
140
+ driver.on("open") do
141
+ logger.debug "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message"
142
+ end
143
+
144
+ driver.on("message") do |event|
145
+ event_data = parse_event_data(event.data).freeze
146
+ protocol_message = Ably::Models::ProtocolMessage.new(event_data)
147
+ logger.debug "WebsocketTransport: Prot msg recv <=: #{protocol_message.action} #{event_data}"
148
+
149
+ if protocol_message.invalid?
150
+ logger.fatal "WebsocketTransport: Invalid Protocol Message received: #{event_data}\nNo action taken"
151
+ else
152
+ __incoming_protocol_msgbus__.publish :protocol_message, protocol_message
153
+ end
154
+ end
155
+ end
156
+
157
+ def client
158
+ connection.client
159
+ end
160
+
161
+ # Used to log transport messages
162
+ def logger
163
+ connection.logger
164
+ end
165
+
166
+ def parse_event_data(data)
167
+ case client.protocol
168
+ when :json
169
+ JSON.parse(data)
170
+ when :msgpack
171
+ MessagePack.unpack(data.pack('C*'))
172
+ else
173
+ client.logger.fatal "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}"
174
+ data
175
+ end
176
+ end
177
+
178
+ def create_pub_sub_message_bus
179
+ Ably::Util::PubSub.new(
180
+ coerce_into: Proc.new do |event|
181
+ raise KeyError, "Expected :protocol_message, :#{event} is disallowed" unless event == :protocol_message
182
+ :protocol_message
183
+ end
184
+ )
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,30 @@
1
+ module Ably::Realtime::Models
2
+ # Nil object for Channels, this object is only used within the internal API of this client library
3
+ # @api private
4
+ class NilChannel
5
+ include Ably::Modules::EventEmitter
6
+ extend Ably::Modules::Enum
7
+ STATE = ruby_enum('STATE', Ably::Realtime::Channel::STATE)
8
+ include Ably::Modules::StateEmitter
9
+ include Ably::Modules::UsesStateMachine
10
+
11
+ attr_reader :state_machine
12
+
13
+ def initialize
14
+ @state_machine = Ably::Realtime::Channel::ChannelStateMachine.new(self)
15
+ @state = STATE(state_machine.current_state)
16
+ end
17
+
18
+ def name
19
+ 'Nil channel'
20
+ end
21
+
22
+ def __incoming_msgbus__
23
+ @__incoming_msgbus__ ||= Ably::Util::PubSub.new
24
+ end
25
+
26
+ def logger
27
+ @logger ||= Ably::Models::NilLogger.new
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,564 @@
1
+ module Ably::Realtime
2
+ # Presence provides access to presence operations and state for the associated Channel
3
+ class Presence
4
+ include Ably::Modules::EventEmitter
5
+ include Ably::Modules::AsyncWrapper
6
+ extend Ably::Modules::Enum
7
+
8
+ STATE = ruby_enum('STATE',
9
+ :initialized,
10
+ :entering,
11
+ :entered,
12
+ :leaving,
13
+ :left,
14
+ :failed
15
+ )
16
+ include Ably::Modules::StateEmitter
17
+
18
+ # {Ably::Realtime::Channel} this Presence object is associated with
19
+ # @return [Ably::Realtime::Channel]
20
+ attr_reader :channel
21
+
22
+ # A unique identifier for this channel client based on their connection, disambiguating situations
23
+ # where a given client_id is present on multiple connections simultaneously.
24
+ # @return [String]
25
+ attr_reader :connection_id
26
+
27
+ # The client_id for the member present on this channel
28
+ # @return [String]
29
+ attr_reader :client_id
30
+
31
+ # The data for the member present on this channel
32
+ # @return [String]
33
+ attr_reader :data
34
+
35
+ def initialize(channel)
36
+ @channel = channel
37
+ @state = STATE.Initialized
38
+ @members = Hash.new
39
+ @subscriptions = Hash.new { |hash, key| hash[key] = [] }
40
+ @client_id = client.client_id
41
+
42
+ setup_event_handlers
43
+ end
44
+
45
+ # Enter this client into this channel. This client will be added to the presence set
46
+ # and presence subscribers will see an enter message for this client.
47
+ #
48
+ # @param [Hash] options an options Hash to specify client data and/or client ID
49
+ # @option options [String] :data optional data (eg a status message) for this member
50
+ # @option options [String] :client_id the optional id of the client.
51
+ # This option is provided to support connections from server instances that act on behalf of
52
+ # multiple client_ids. In order to be able to enter the channel with this method, the client
53
+ # library must have been instanced either with a key, or with a token bound to the wildcard clientId.
54
+ #
55
+ # @yield [Ably::Realtime::Presence] On success, will call the block with this {Ably::Realtime::Presence} object
56
+ # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
57
+ #
58
+ def enter(options = {}, &success_block)
59
+ @client_id = options.fetch(:client_id, client_id)
60
+ @data = options.fetch(:data, nil)
61
+ deferrable = EventMachine::DefaultDeferrable.new
62
+
63
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
64
+ return deferrable_succeed(deferrable, &success_block) if state == STATE.Entered
65
+
66
+ ensure_channel_attached(deferrable) do
67
+ if entering?
68
+ once_or_if(STATE.Entered, else: proc { |args| deferrable_fail deferrable, *args }) do
69
+ deferrable_succeed deferrable, &success_block
70
+ end
71
+ else
72
+ change_state STATE.Entering
73
+ send_protocol_message_and_transition_state_to(
74
+ Ably::Models::PresenceMessage::ACTION.Enter,
75
+ deferrable: deferrable,
76
+ target_state: STATE.Entered,
77
+ client_id: client_id,
78
+ data: data,
79
+ failed_state: STATE.Failed,
80
+ &success_block
81
+ )
82
+ end
83
+ end
84
+ end
85
+
86
+ # Enter the specified client_id into this channel. The given client will be added to the
87
+ # presence set and presence subscribers will see a corresponding presence message.
88
+ # This method is provided to support connections (e.g. connections from application
89
+ # server instances) that act on behalf of multiple client_ids. In order to be able to
90
+ # enter the channel with this method, the client library must have been instanced
91
+ # either with a key, or with a token bound to the wildcard client_id
92
+ #
93
+ # @param [String] client_id id of the client
94
+ #
95
+ # @param [Hash] options an options Hash for this client event
96
+ # @option options [String] :data optional data (eg a status message) for this member
97
+ #
98
+ # @yield [Ably::Realtime::Presence] On success, will call the block with this {Ably::Realtime::Presence} object
99
+ # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
100
+ #
101
+ def enter_client(client_id, options = {}, &success_block)
102
+ raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
103
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
104
+
105
+ send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Enter, client_id, options, &success_block)
106
+ end
107
+
108
+ # Leave this client from this channel. This client will be removed from the presence
109
+ # set and presence subscribers will see a leave message for this client.
110
+ #
111
+ # @param [Hash,String] options an options Hash to specify client data and/or client ID
112
+ # @option options [String] :data optional data (eg a status message) for this member
113
+ #
114
+ # @yield (see Presence#enter)
115
+ # @return (see Presence#enter)
116
+ #
117
+ def leave(options = {}, &success_block)
118
+ @data = options.fetch(:data, data) # nil value defaults leave data to existing value
119
+ deferrable = EventMachine::DefaultDeferrable.new
120
+
121
+ raise Ably::Exceptions::Standard.new('Unable to leave presence channel that is not entered', 400, 91002) unless able_to_leave?
122
+ return deferrable_succeed(deferrable, &success_block) if state == STATE.Left
123
+
124
+ ensure_channel_attached(deferrable) do
125
+ if leaving?
126
+ once_or_if(STATE.Left, else: proc { |error|deferrable_fail deferrable, *args }) do
127
+ deferrable_succeed deferrable, &success_block
128
+ end
129
+ else
130
+ change_state STATE.Leaving
131
+ send_protocol_message_and_transition_state_to(
132
+ Ably::Models::PresenceMessage::ACTION.Leave,
133
+ deferrable: deferrable,
134
+ target_state: STATE.Left,
135
+ client_id: client_id,
136
+ data: data,
137
+ failed_state: STATE.Failed,
138
+ &success_block
139
+ )
140
+ end
141
+ end
142
+ end
143
+
144
+ # Leave a given client_id from this channel. This client will be removed from the
145
+ # presence set and presence subscribers will see a leave message for this client.
146
+ #
147
+ # @param (see Presence#enter_client)
148
+ # @option options (see Presence#enter_client)
149
+ #
150
+ # @yield (see Presence#enter_client)
151
+ # @return (see Presence#enter_client)
152
+ #
153
+ def leave_client(client_id, options = {}, &success_block)
154
+ raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
155
+ raise Ably::Exceptions::Standard.new('Unable to leave presence channel without a client_id', 400, 91000) unless client_id
156
+
157
+ send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Leave, client_id, options, &success_block)
158
+ end
159
+
160
+ # Update the presence data for this client. If the client is not already a member of
161
+ # the presence set it will be added, and presence subscribers will see an enter or
162
+ # update message for this client.
163
+ #
164
+ # @param [Hash,String] options an options Hash to specify client data
165
+ # @option options [String] :data optional data (eg a status message) for this member
166
+ #
167
+ # @yield (see Presence#enter)
168
+ # @return (see Presence#enter)
169
+ #
170
+ def update(options = {}, &success_block)
171
+ @data = options.fetch(:data, nil)
172
+ deferrable = EventMachine::DefaultDeferrable.new
173
+
174
+ ensure_channel_attached(deferrable) do
175
+ send_protocol_message_and_transition_state_to(
176
+ Ably::Models::PresenceMessage::ACTION.Update,
177
+ deferrable: deferrable,
178
+ target_state: STATE.Entered,
179
+ client_id: client_id,
180
+ data: data,
181
+ &success_block
182
+ )
183
+ end
184
+ end
185
+
186
+ # Update the presence data for a specified client_id into this channel.
187
+ # If the client is not already a member of the presence set it will be added, and
188
+ # presence subscribers will see an enter or update message for this client.
189
+ # As with {#enter_client}, the connection must be authenticated in a way that
190
+ # enables it to represent an arbitrary clientId.
191
+ #
192
+ # @param (see Presence#enter_client)
193
+ # @option options (see Presence#enter_client)
194
+ #
195
+ # @yield (see Presence#enter_client)
196
+ # @return (see Presence#enter_client)
197
+ #
198
+ def update_client(client_id, options = {}, &success_block)
199
+ raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
200
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
201
+
202
+ send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Update, client_id, options, &success_block)
203
+ end
204
+
205
+ # Get the presence state for this Channel.
206
+ #
207
+ # @param [Hash,String] options an options Hash to filter members
208
+ # @option options [String] :client_id optional client_id for the member
209
+ # @option options [String] :connection_id optional connection_id for the member
210
+ # @option options [String] :wait_for_sync defaults to true, if false the get method returns the current list of members and does not wait for the presence sync to complete
211
+ #
212
+ # @yield [Array<Ably::Models::PresenceMessage>] array of members or the member
213
+ #
214
+ # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
215
+ #
216
+ def get(options = {})
217
+ wait_for_sync = options.fetch(:wait_for_sync, true)
218
+ deferrable = EventMachine::DefaultDeferrable.new
219
+
220
+ ensure_channel_attached(deferrable) do
221
+ result_block = proc do
222
+ members.map { |key, presence| presence }.tap do |filtered_members|
223
+ filtered_members.keep_if { |presence| presence.connection_id == options[:connection_id] } if options[:connection_id]
224
+ filtered_members.keep_if { |presence| presence.client_id == options[:client_id] } if options[:client_id]
225
+ end.tap do |current_members|
226
+ yield current_members if block_given?
227
+ deferrable.succeed current_members
228
+ end
229
+ end
230
+
231
+ if !wait_for_sync || sync_complete?
232
+ result_block.call
233
+ else
234
+ sync_pubsub.once(:done) do
235
+ result_block.call
236
+ end
237
+
238
+ sync_pubsub.once(:failed) do |error|
239
+ deferrable.fail error
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ # Subscribe to presence events on the associated Channel.
246
+ # This implicitly attaches the Channel if it is not already attached.
247
+ #
248
+ # @param action [Ably::Models::PresenceMessage::ACTION] Optional, the state change action to subscribe to. Defaults to all presence actions
249
+ # @yield [Ably::Models::PresenceMessage] For each presence state change event, the block is called
250
+ #
251
+ # @return [void]
252
+ #
253
+ def subscribe(action = :all, &callback)
254
+ ensure_channel_attached do
255
+ subscriptions[message_action_key(action)] << callback
256
+ end
257
+ end
258
+
259
+ # Unsubscribe the matching block for presence events on the associated Channel.
260
+ # If a block is not provided, all subscriptions will be unsubscribed
261
+ #
262
+ # @param action [Ably::Models::PresenceMessage::ACTION] Optional, the state change action to subscribe to. Defaults to all presence actions
263
+ #
264
+ # @return [void]
265
+ #
266
+ def unsubscribe(action = :all, &callback)
267
+ if message_action_key(action) == :all
268
+ subscriptions.keys
269
+ else
270
+ Array(message_action_key(action))
271
+ end.each do |key|
272
+ subscriptions[key].delete_if do |block|
273
+ !block_given? || callback == block
274
+ end
275
+ end
276
+ end
277
+
278
+ # Return the presence messages history for the channel
279
+ #
280
+ # @param (see Ably::Rest::Presence#history)
281
+ # @option options (see Ably::Rest::Presence#history)
282
+ #
283
+ # @yield [Ably::Models::PaginatedResource<Ably::Models::PresenceMessage>] An Array of {Ably::Models::PresenceMessage} objects that supports paging (#next_page, #first_page)
284
+ #
285
+ # @return [EventMachine::Deferrable]
286
+ #
287
+ def history(options = {}, &callback)
288
+ async_wrap(callback) do
289
+ rest_presence.history(options.merge(async_blocking_operations: true))
290
+ end
291
+ end
292
+
293
+ # When attaching to a channel that has members present, the client and server
294
+ # initiate a sync automatically so that the client has a complete list of members.
295
+ #
296
+ # Whilst this sync is happening, this method returns false
297
+ #
298
+ # @return [Boolean]
299
+ def sync_complete?
300
+ sync_complete
301
+ end
302
+
303
+ # Expect SYNC ProtocolMessages with a list of current members on this channel from the server
304
+ #
305
+ # @return [void]
306
+ #
307
+ # @api private
308
+ def sync_started
309
+ @sync_complete = false
310
+
311
+ sync_pubsub.once(:sync_complete) do
312
+ sync_changes_backlog.each do |presence_message|
313
+ apply_member_presence_changes presence_message
314
+ end
315
+ sync_completed
316
+ sync_pubsub.trigger :done
317
+ end
318
+
319
+ channel.once_or_if [:detached, :failed] do |error|
320
+ sync_completed
321
+ sync_pubsub.trigger :failed, error
322
+ end
323
+ end
324
+
325
+ # The server has indicated that no members are present on this channel and no SYNC is expected,
326
+ # or that the SYNC has now completed
327
+ #
328
+ # @return [void]
329
+ #
330
+ # @api private
331
+ def sync_completed
332
+ @sync_complete = true
333
+ @sync_changes_backlog = []
334
+ end
335
+
336
+ # Update the SYNC serial from the ProtocolMessage so that SYNC can be resumed.
337
+ # If the serial is nil, or the part after the first : is empty, then the SYNC is complete
338
+ #
339
+ # @return [void]
340
+ #
341
+ # @api private
342
+ def update_sync_serial(serial)
343
+ @sync_serial = serial
344
+ sync_pubsub.trigger :sync_complete if sync_serial_cursor_at_end?
345
+ end
346
+
347
+ # @!attribute [r] __incoming_msgbus__
348
+ # @return [Ably::Util::PubSub] Client library internal channel incoming message bus
349
+ # @api private
350
+ def __incoming_msgbus__
351
+ @__incoming_msgbus__ ||= Ably::Util::PubSub.new(
352
+ coerce_into: Proc.new { |event| Ably::Models::ProtocolMessage::ACTION(event) }
353
+ )
354
+ end
355
+
356
+ private
357
+ attr_reader :members, :subscriptions, :sync_serial, :sync_complete
358
+
359
+
360
+ # A simple PubSub class used to publish synchronisation state changes
361
+ def sync_pubsub
362
+ @sync_pubsub ||= Ably::Util::PubSub.new
363
+ end
364
+
365
+ # During a SYNC of presence members, all enter, update and leave events are queued for processing once the SYNC is complete
366
+ def sync_changes_backlog
367
+ @sync_changes_backlog ||= []
368
+ end
369
+
370
+ # When channel serial in ProtocolMessage SYNC is nil or
371
+ # an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'
372
+ # then there are no more SYNC messages to come
373
+ def sync_serial_cursor_at_end?
374
+ sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
375
+ end
376
+
377
+ def able_to_leave?
378
+ entering? || entered?
379
+ end
380
+
381
+ def setup_event_handlers
382
+ __incoming_msgbus__.subscribe(:presence, :sync) do |presence_message|
383
+ presence_message.decode self.channel
384
+ update_members_from_presence_message presence_message
385
+ end
386
+
387
+ channel.on(Channel::STATE.Detaching) do
388
+ change_state STATE.Leaving
389
+ end
390
+
391
+ channel.on(Channel::STATE.Detached) do
392
+ change_state STATE.Left
393
+ end
394
+
395
+ channel.on(Channel::STATE.Failed) do
396
+ change_state STATE.Failed unless left? || initialized?
397
+ end
398
+
399
+ on(STATE.Entered) do |message|
400
+ @connection_id = message.connection_id
401
+ end
402
+ end
403
+
404
+ # @return [Ably::Models::PresenceMessage] presence message is returned allowing callbacks to be added
405
+ def send_presence_protocol_message(presence_action, client_id, options = {})
406
+ presence_message = create_presence_message(presence_action, client_id, options)
407
+ unless presence_message.client_id
408
+ raise Ably::Exceptions::Standard.new('Unable to enter create presence message without a client_id', 400, 91000)
409
+ end
410
+
411
+ protocol_message = {
412
+ action: Ably::Models::ProtocolMessage::ACTION.Presence,
413
+ channel: channel.name,
414
+ presence: [presence_message]
415
+ }
416
+
417
+ client.connection.send_protocol_message protocol_message
418
+
419
+ presence_message
420
+ end
421
+
422
+ def create_presence_message(action, client_id, options = {})
423
+ model = {
424
+ action: Ably::Models::PresenceMessage.ACTION(action).to_i,
425
+ clientId: client_id
426
+ }
427
+ model.merge!(data: options.fetch(:data)) if options.has_key?(:data)
428
+
429
+ Ably::Models::PresenceMessage.new(model, nil).tap do |presence_message|
430
+ presence_message.encode self.channel
431
+ end
432
+ end
433
+
434
+ def update_members_from_presence_message(presence_message)
435
+ unless presence_message.connection_id
436
+ Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, 80013)
437
+ end
438
+
439
+ if sync_complete?
440
+ apply_member_presence_changes presence_message
441
+ else
442
+ if presence_message.action == Ably::Models::PresenceMessage::ACTION.Present
443
+ add_presence_member presence_message
444
+ publish_presence_member_state_change presence_message
445
+ else
446
+ sync_changes_backlog << presence_message
447
+ end
448
+ end
449
+ end
450
+
451
+ def apply_member_presence_changes(presence_message)
452
+ case presence_message.action
453
+ when Ably::Models::PresenceMessage::ACTION.Enter, Ably::Models::PresenceMessage::ACTION.Update
454
+ add_presence_member presence_message
455
+ when Ably::Models::PresenceMessage::ACTION.Leave
456
+ remove_presence_member presence_message
457
+ else
458
+ Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence_message.action}", 400, 80013)
459
+ end
460
+
461
+ publish_presence_member_state_change presence_message
462
+ end
463
+
464
+ def add_presence_member(presence_message)
465
+ members[presence_message.member_key] = presence_message
466
+ end
467
+
468
+ def remove_presence_member(presence_message)
469
+ members.delete presence_message.member_key
470
+ end
471
+
472
+ def publish_presence_member_state_change(presence_message)
473
+ subscriptions[:all].each { |cb| cb.call(presence_message) }
474
+ subscriptions[presence_message.action].each { |cb| cb.call(presence_message) }
475
+ end
476
+
477
+ def ensure_channel_attached(deferrable = nil)
478
+ if channel.attached?
479
+ yield
480
+ else
481
+ attach_channel_then { yield }
482
+ end
483
+ deferrable
484
+ end
485
+
486
+ def send_protocol_message_and_transition_state_to(action, options = {}, &success_block)
487
+ deferrable = options.fetch(:deferrable) { raise ArgumentError, 'option :deferrable is required' }
488
+ client_id = options.fetch(:client_id) { raise ArgumentError, 'option :client_id is required' }
489
+ target_state = options.fetch(:target_state, nil)
490
+ failed_state = options.fetch(:failed_state, nil)
491
+
492
+ protocol_message_options = if options.has_key?(:data)
493
+ { data: options.fetch(:data) }
494
+ else
495
+ { }
496
+ end
497
+
498
+ send_presence_protocol_message(action, client_id, protocol_message_options).tap do |protocol_message|
499
+ protocol_message.callback do |message|
500
+ change_state target_state, message if target_state
501
+ deferrable_succeed deferrable, &success_block
502
+ end
503
+
504
+ protocol_message.errback do |message, error|
505
+ change_state failed_state, error if failed_state
506
+ deferrable_fail deferrable, error
507
+ end
508
+ end
509
+ end
510
+
511
+ def deferrable_succeed(deferrable, *args)
512
+ yield self, *args if block_given?
513
+ EventMachine.next_tick { deferrable.succeed self, *args } # allow callback to be added to the returned Deferrable before calling succeed
514
+ deferrable
515
+ end
516
+
517
+ def deferrable_fail(deferrable, *args)
518
+ yield self, *args if block_given?
519
+ EventMachine.next_tick { deferrable.fail self, *args } # allow errback to be added to the returned Deferrable
520
+ deferrable
521
+ end
522
+
523
+ def send_presence_action_for_client(action, client_id, options = {}, &success_block)
524
+ deferrable = EventMachine::DefaultDeferrable.new
525
+
526
+ ensure_channel_attached(deferrable) do
527
+ send_presence_protocol_message(action, client_id, options).tap do |protocol_message|
528
+ protocol_message.callback { |message| deferrable_succeed deferrable, &success_block }
529
+ protocol_message.errback { |message| deferrable_fail deferrable }
530
+ end
531
+ end
532
+ end
533
+
534
+ def attach_channel_then
535
+ if channel.detached? || channel.failed?
536
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel in detached or failed action', 400, 91001)
537
+ else
538
+ channel.once(Channel::STATE.Attached) { yield }
539
+ channel.attach
540
+ end
541
+ end
542
+
543
+ def client
544
+ channel.client
545
+ end
546
+
547
+ def rest_presence
548
+ client.rest_client.channel(channel.name).presence
549
+ end
550
+
551
+ # Used by {Ably::Modules::StateEmitter} to debug action changes
552
+ def logger
553
+ client.logger
554
+ end
555
+
556
+ def message_action_key(action)
557
+ if action == :all
558
+ :all
559
+ else
560
+ Ably::Models::PresenceMessage.ACTION(action)
561
+ end
562
+ end
563
+ end
564
+ end