ably-pubsub-core 0.0.1.pre

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ably/agent.rb +8 -0
  3. data/lib/ably/auth.rb +791 -0
  4. data/lib/ably/exceptions.rb +169 -0
  5. data/lib/ably/logger.rb +119 -0
  6. data/lib/ably/models/auth_details.rb +45 -0
  7. data/lib/ably/models/channel_details.rb +69 -0
  8. data/lib/ably/models/channel_metrics.rb +107 -0
  9. data/lib/ably/models/channel_occupancy.rb +50 -0
  10. data/lib/ably/models/channel_options.rb +120 -0
  11. data/lib/ably/models/channel_state_change.rb +84 -0
  12. data/lib/ably/models/channel_status.rb +63 -0
  13. data/lib/ably/models/cipher_params.rb +137 -0
  14. data/lib/ably/models/connection_details.rb +149 -0
  15. data/lib/ably/models/connection_state_change.rb +83 -0
  16. data/lib/ably/models/delta_extras.rb +28 -0
  17. data/lib/ably/models/device_details.rb +126 -0
  18. data/lib/ably/models/device_push_details.rb +94 -0
  19. data/lib/ably/models/error_info.rb +104 -0
  20. data/lib/ably/models/http_paginated_response.rb +112 -0
  21. data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
  22. data/lib/ably/models/message.rb +223 -0
  23. data/lib/ably/models/message_encoders/base.rb +135 -0
  24. data/lib/ably/models/message_encoders/base64.rb +40 -0
  25. data/lib/ably/models/message_encoders/cipher.rb +84 -0
  26. data/lib/ably/models/message_encoders/json.rb +34 -0
  27. data/lib/ably/models/message_encoders/utf8.rb +26 -0
  28. data/lib/ably/models/nil_logger.rb +20 -0
  29. data/lib/ably/models/paginated_result.rb +201 -0
  30. data/lib/ably/models/presence_message.rb +244 -0
  31. data/lib/ably/models/protocol_message.rb +288 -0
  32. data/lib/ably/models/push_channel_subscription.rb +114 -0
  33. data/lib/ably/models/stats.rb +242 -0
  34. data/lib/ably/models/stats_types.rb +107 -0
  35. data/lib/ably/models/token_details.rb +148 -0
  36. data/lib/ably/models/token_request.rb +180 -0
  37. data/lib/ably/modules/ably.rb +26 -0
  38. data/lib/ably/modules/async_wrapper.rb +69 -0
  39. data/lib/ably/modules/channels_collection.rb +105 -0
  40. data/lib/ably/modules/conversions.rb +153 -0
  41. data/lib/ably/modules/encodeable.rb +111 -0
  42. data/lib/ably/modules/enum.rb +230 -0
  43. data/lib/ably/modules/event_emitter.rb +200 -0
  44. data/lib/ably/modules/event_machine_helpers.rb +26 -0
  45. data/lib/ably/modules/exception_codes.rb +128 -0
  46. data/lib/ably/modules/http_helpers.rb +41 -0
  47. data/lib/ably/modules/message_emitter.rb +78 -0
  48. data/lib/ably/modules/message_pack.rb +14 -0
  49. data/lib/ably/modules/model_common.rb +84 -0
  50. data/lib/ably/modules/safe_deferrable.rb +83 -0
  51. data/lib/ably/modules/safe_yield.rb +41 -0
  52. data/lib/ably/modules/state_emitter.rb +181 -0
  53. data/lib/ably/modules/state_machine.rb +59 -0
  54. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  55. data/lib/ably/modules/uses_state_machine.rb +111 -0
  56. data/lib/ably/realtime/auth.rb +283 -0
  57. data/lib/ably/realtime/channel/channel_manager.rb +272 -0
  58. data/lib/ably/realtime/channel/channel_properties.rb +40 -0
  59. data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
  60. data/lib/ably/realtime/channel/publisher.rb +82 -0
  61. data/lib/ably/realtime/channel/push_channel.rb +69 -0
  62. data/lib/ably/realtime/channel.rb +440 -0
  63. data/lib/ably/realtime/channels.rb +71 -0
  64. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
  65. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
  66. data/lib/ably/realtime/client.rb +363 -0
  67. data/lib/ably/realtime/connection/connection_manager.rb +594 -0
  68. data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
  69. data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
  70. data/lib/ably/realtime/connection.rb +738 -0
  71. data/lib/ably/realtime/models/nil_channel.rb +30 -0
  72. data/lib/ably/realtime/presence/members_map.rb +395 -0
  73. data/lib/ably/realtime/presence/presence_manager.rb +78 -0
  74. data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
  75. data/lib/ably/realtime/presence.rb +502 -0
  76. data/lib/ably/realtime/push/admin.rb +75 -0
  77. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  78. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  79. data/lib/ably/realtime/push.rb +25 -0
  80. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  81. data/lib/ably/realtime.rb +60 -0
  82. data/lib/ably/rest/channel/push_channel.rb +59 -0
  83. data/lib/ably/rest/channel.rb +194 -0
  84. data/lib/ably/rest/channels.rb +47 -0
  85. data/lib/ably/rest/client.rb +727 -0
  86. data/lib/ably/rest/middleware/encoder.rb +49 -0
  87. data/lib/ably/rest/middleware/exceptions.rb +53 -0
  88. data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  89. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
  90. data/lib/ably/rest/middleware/logger.rb +58 -0
  91. data/lib/ably/rest/middleware/parse_json.rb +27 -0
  92. data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
  93. data/lib/ably/rest/presence.rb +113 -0
  94. data/lib/ably/rest/push/admin.rb +58 -0
  95. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  96. data/lib/ably/rest/push/device_registrations.rb +114 -0
  97. data/lib/ably/rest/push.rb +25 -0
  98. data/lib/ably/rest.rb +40 -0
  99. data/lib/ably/util/ably_extensions.rb +29 -0
  100. data/lib/ably/util/crypto.rb +134 -0
  101. data/lib/ably/util/pub_sub.rb +45 -0
  102. data/lib/ably/util/safe_deferrable.rb +44 -0
  103. data/lib/ably/version.rb +7 -0
  104. data/lib/ably.rb +16 -0
  105. metadata +497 -0
@@ -0,0 +1,26 @@
1
+ require 'eventmachine'
2
+
3
+ module Ably::Modules
4
+ # EventMachineHelpers module provides common private methods to classes simplifying interaction with EventMachine
5
+ module EventMachineHelpers
6
+ private
7
+
8
+ # This method allows looped blocks to be run at the next EventMachine tick
9
+ # @example
10
+ # x = 0
11
+ # less_than_3 = -> { x < 3 }
12
+ # non_blocking_loop_while(less_than_3) do
13
+ # x += 1
14
+ # end
15
+ def non_blocking_loop_while(lambda_condition, &execution_block)
16
+ if lambda_condition.call
17
+ EventMachine.next_tick do
18
+ if lambda_condition.call # ensure condition is still met following #next_tick
19
+ yield
20
+ non_blocking_loop_while(lambda_condition, &execution_block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,128 @@
1
+ # This file is generated by running `rake :generate_error_codes`
2
+ # Do not manually modify this file
3
+ # Generated at: 2018-09-18 18:28:58 UTC
4
+ #
5
+ module Ably
6
+ module Exceptions
7
+ module Codes
8
+ NO_ERROR = 10000
9
+ BAD_REQUEST = 40000
10
+ INVALID_REQUEST_BODY = 40001
11
+ INVALID_PARAMETER_NAME = 40002
12
+ INVALID_PARAMETER_VALUE = 40003
13
+ INVALID_HEADER = 40004
14
+ INVALID_CREDENTIAL = 40005
15
+ INVALID_CONNECTION_ID = 40006
16
+ INVALID_MESSAGE_ID = 40007
17
+ INVALID_CONTENT_LENGTH = 40008
18
+ MAXIMUM_MESSAGE_LENGTH_EXCEEDED = 40009
19
+ INVALID_CHANNEL_NAME = 40010
20
+ STALE_RING_STATE = 40011
21
+ INVALID_CLIENT_ID = 40012
22
+ INVALID_MESSAGE_DATA_OR_ENCODING = 40013
23
+ RESOURCE_DISPOSED = 40014
24
+ INVALID_DEVICE_ID = 40015
25
+ BATCH_ERROR = 40020
26
+ INVALID_PUBLISH_REQUEST_UNSPECIFIED = 40030
27
+ INVALID_PUBLISH_REQUEST_INVALID_CLIENTSPECIFIED_ID = 40031
28
+ UNAUTHORIZED = 40100
29
+ INVALID_CREDENTIALS = 40101
30
+ INCOMPATIBLE_CREDENTIALS = 40102
31
+ INVALID_USE_OF_BASIC_AUTH_OVER_NONTLS_TRANSPORT = 40103
32
+ TIMESTAMP_NOT_CURRENT = 40104
33
+ NONCE_VALUE_REPLAYED = 40105
34
+ UNABLE_TO_OBTAIN_CREDENTIALS_FROM_GIVEN_PARAMETERS = 40106
35
+ ACCOUNT_DISABLED = 40110
36
+ ACCOUNT_RESTRICTED_CONNECTION_LIMITS_EXCEEDED = 40111
37
+ ACCOUNT_BLOCKED_MESSAGE_LIMITS_EXCEEDED = 40112
38
+ ACCOUNT_BLOCKED = 40113
39
+ ACCOUNT_RESTRICTED_CHANNEL_LIMITS_EXCEEDED = 40114
40
+ APPLICATION_DISABLED = 40120
41
+ KEY_ERROR_UNSPECIFIED = 40130
42
+ KEY_REVOKED = 40131
43
+ KEY_EXPIRED = 40132
44
+ KEY_DISABLED = 40133
45
+ TOKEN_ERROR_UNSPECIFIED = 40140
46
+ TOKEN_REVOKED = 40141
47
+ TOKEN_EXPIRED = 40142
48
+ TOKEN_UNRECOGNISED = 40143
49
+ INVALID_JWT_FORMAT = 40144
50
+ INVALID_TOKEN_FORMAT = 40145
51
+ CONNECTION_BLOCKED_LIMITS_EXCEEDED = 40150
52
+ OPERATION_NOT_PERMITTED_WITH_PROVIDED_CAPABILITY = 40160
53
+ ERROR_FROM_CLIENT_TOKEN_CALLBACK = 40170
54
+ FORBIDDEN = 40300
55
+ ACCOUNT_DOES_NOT_PERMIT_TLS_CONNECTION = 40310
56
+ OPERATION_REQUIRES_TLS_CONNECTION = 40311
57
+ APPLICATION_REQUIRES_AUTHENTICATION = 40320
58
+ UNABLE_TO_ACTIVATE_ACCOUNT_DUE_TO_PLACEMENT_CONSTRAINT_UNSPECIFIED = 40330
59
+ UNABLE_TO_ACTIVATE_ACCOUNT_DUE_TO_PLACEMENT_CONSTRAINT_INCOMPATIBLE_ENVIRONMENT = 40331
60
+ UNABLE_TO_ACTIVATE_ACCOUNT_DUE_TO_PLACEMENT_CONSTRAINT_INCOMPATIBLE_SITE = 40332
61
+ NOT_FOUND = 40400
62
+ METHOD_NOT_ALLOWED = 40500
63
+ RATE_LIMIT_EXCEEDED_NONFATAL_REQUEST_REJECTED_UNSPECIFIED = 42910
64
+ MAX_PERCONNECTION_PUBLISH_RATE_LIMIT_EXCEEDED_NONFATAL_UNABLE_TO_PUBLISH_MESSAGE = 42911
65
+ RATE_LIMIT_EXCEEDED_FATAL = 42920
66
+ MAX_PERCONNECTION_PUBLISH_RATE_LIMIT_EXCEEDED_FATAL_CLOSING_CONNECTION = 42921
67
+ INTERNAL_ERROR = 50000
68
+ INTERNAL_CHANNEL_ERROR = 50001
69
+ INTERNAL_CONNECTION_ERROR = 50002
70
+ TIMEOUT_ERROR = 50003
71
+ REQUEST_FAILED_DUE_TO_OVERLOADED_INSTANCE = 50004
72
+ REACTOR_OPERATION_FAILED = 70000
73
+ REACTOR_OPERATION_FAILED_POST_OPERATION_FAILED = 70001
74
+ REACTOR_OPERATION_FAILED_POST_OPERATION_RETURNED_UNEXPECTED_CODE = 70002
75
+ REACTOR_OPERATION_FAILED_MAXIMUM_NUMBER_OF_CONCURRENT_INFLIGHT_REQUESTS_EXCEEDED = 70003
76
+ EXCHANGE_ERROR_UNSPECIFIED = 71000
77
+ FORCED_REATTACHMENT_DUE_TO_PERMISSIONS_CHANGE = 71001
78
+ EXCHANGE_PUBLISHER_ERROR_UNSPECIFIED = 71100
79
+ NO_SUCH_PUBLISHER = 71101
80
+ PUBLISHER_NOT_ENABLED_AS_AN_EXCHANGE_PUBLISHER = 71102
81
+ EXCHANGE_PRODUCT_ERROR_UNSPECIFIED = 71200
82
+ NO_SUCH_PRODUCT = 71201
83
+ PRODUCT_DISABLED = 71202
84
+ NO_SUCH_CHANNEL_IN_THIS_PRODUCT = 71203
85
+ EXCHANGE_SUBSCRIPTION_ERROR_UNSPECIFIED = 71300
86
+ SUBSCRIPTION_DISABLED = 71301
87
+ REQUESTER_HAS_NO_SUBSCRIPTION_TO_THIS_PRODUCT = 71302
88
+ CONNECTION_FAILED = 80000
89
+ CONNECTION_FAILED_NO_COMPATIBLE_TRANSPORT = 80001
90
+ CONNECTION_SUSPENDED = 80002
91
+ DISCONNECTED = 80003
92
+ ALREADY_CONNECTED = 80004
93
+ INVALID_CONNECTION_ID_REMOTE_NOT_FOUND = 80005
94
+ UNABLE_TO_RECOVER_CONNECTION_MESSAGES_EXPIRED = 80006
95
+ UNABLE_TO_RECOVER_CONNECTION_MESSAGE_LIMIT_EXCEEDED = 80007
96
+ UNABLE_TO_RECOVER_CONNECTION_CONNECTION_EXPIRED = 80008
97
+ CONNECTION_NOT_ESTABLISHED_NO_TRANSPORT_HANDLE = 80009
98
+ INVALID_OPERATION_INVALID_TRANSPORT_HANDLE = 80010
99
+ UNABLE_TO_RECOVER_CONNECTION_INCOMPATIBLE_AUTH_PARAMS = 80011
100
+ UNABLE_TO_RECOVER_CONNECTION_INVALID_OR_UNSPECIFIED_CONNECTION_SERIAL = 80012
101
+ PROTOCOL_ERROR = 80013
102
+ CONNECTION_TIMED_OUT = 80014
103
+ INCOMPATIBLE_CONNECTION_PARAMETERS = 80015
104
+ OPERATION_ON_SUPERSEDED_TRANSPORT = 80016
105
+ CONNECTION_CLOSED = 80017
106
+ INVALID_CONNECTION_ID_INVALID_FORMAT = 80018
107
+ CLIENT_CONFIGURED_AUTHENTICATION_PROVIDER_REQUEST_FAILED = 80019
108
+ CONTINUITY_LOSS_DUE_TO_MAXIMUM_SUBSCRIBE_MESSAGE_RATE_EXCEEDED = 80020
109
+ CLIENT_RESTRICTION_NOT_SATISFIED = 80030
110
+ CHANNEL_OPERATION_FAILED = 90000
111
+ CHANNEL_OPERATION_FAILED_INVALID_CHANNEL_STATE = 90001
112
+ CHANNEL_OPERATION_FAILED_EPOCH_EXPIRED_OR_NEVER_EXISTED = 90002
113
+ UNABLE_TO_RECOVER_CHANNEL_MESSAGES_EXPIRED = 90003
114
+ UNABLE_TO_RECOVER_CHANNEL_MESSAGE_LIMIT_EXCEEDED = 90004
115
+ UNABLE_TO_RECOVER_CHANNEL_NO_MATCHING_EPOCH = 90005
116
+ UNABLE_TO_RECOVER_CHANNEL_UNBOUNDED_REQUEST = 90006
117
+ CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER = 90007
118
+ MAXIMUM_NUMBER_OF_CHANNELS_PER_CONNECTION_EXCEEDED = 90010
119
+ UNABLE_TO_ENTER_PRESENCE_CHANNEL_NO_CLIENTID = 91000
120
+ UNABLE_TO_ENTER_PRESENCE_CHANNEL_INVALID_CHANNEL_STATE = 91001
121
+ UNABLE_TO_LEAVE_PRESENCE_CHANNEL_THAT_IS_NOT_ENTERED = 91002
122
+ UNABLE_TO_ENTER_PRESENCE_CHANNEL_MAXIMUM_MEMBER_LIMIT_EXCEEDED = 91003
123
+ UNABLE_TO_AUTOMATICALLY_REENTER_PRESENCE_CHANNEL = 91004
124
+ PRESENCE_STATE_IS_OUT_OF_SYNC = 91005
125
+ MEMBER_IMPLICITLY_LEFT_PRESENCE_CHANNEL_CONNECTION_CLOSED = 91100
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,41 @@
1
+ require 'base64'
2
+
3
+ require 'ably/version'
4
+
5
+ require 'ably/rest/middleware/encoder'
6
+ require 'ably/rest/middleware/external_exceptions'
7
+ require 'ably/rest/middleware/fail_if_unsupported_mime_type'
8
+ require 'ably/rest/middleware/logger'
9
+ require 'ably/rest/middleware/parse_json'
10
+ require 'ably/rest/middleware/parse_message_pack'
11
+
12
+ module Ably::Modules
13
+ # HttpHelpers provides common private methods to classes to simplify HTTP interactions with Ably
14
+ module HttpHelpers
15
+ protected
16
+ def encode64(text)
17
+ Base64.encode64(text).gsub("\n", '')
18
+ end
19
+
20
+ def user_agent
21
+ "Ably Ruby client #{Ably::VERSION} (https://www.ably.io)"
22
+ end
23
+
24
+ def setup_outgoing_middleware(builder)
25
+ # Convert request params to "www-form-urlencoded"
26
+ builder.use Ably::Rest::Middleware::Encoder
27
+ end
28
+
29
+ def setup_incoming_middleware(builder, logger, options = {})
30
+ builder.use Ably::Rest::Middleware::Logger, logger
31
+
32
+ # Parse JSON / MsgPack response bodies. ParseJson must be first (default) parsing middleware
33
+ if options[:fail_if_unsupported_mime_type] == true
34
+ builder.use Ably::Rest::Middleware::FailIfUnsupportedMimeType
35
+ end
36
+
37
+ builder.use Ably::Rest::Middleware::ParseJson
38
+ builder.use Ably::Rest::Middleware::ParseMessagePack
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,78 @@
1
+ require 'ably/util/pub_sub'
2
+
3
+ module Ably::Modules
4
+ # Message emitter, subscriber and unsubscriber (Pub/Sub) functionality common to Channels and Presence
5
+ # In addition to standard Pub/Sub functionality, it allows subscribers to subscribe to :all.
6
+ module MessageEmitter
7
+
8
+ include Ably::Modules::SafeYield
9
+
10
+ # Subscribe to events on this object
11
+ #
12
+ # @param names [String,Symbol] Optional, the event name(s) to subscribe to. Defaults to `:all` events
13
+ # @yield [Object] For each event, the provided block is called with the event payload object
14
+ #
15
+ # @return [void]
16
+ #
17
+ def subscribe(*names, &callback)
18
+ raise ArgumentError, 'Block required to subscribe to events' unless block_given?
19
+ names = :all unless names && !names.empty?
20
+ Array(names).uniq.each do |name|
21
+ message_emitter_subscriptions[message_emitter_subscriptions_message_name_key(name)] << callback
22
+ end
23
+ end
24
+
25
+ # Unsubscribe the matching block for events on the this object.
26
+ # If a block is not provided, all subscriptions will be unsubscribed
27
+ #
28
+ # @param names [String,Symbol] Optional, the event name(s) to unsubscribe from. Defaults to `:all` events
29
+ #
30
+ # @return [void]
31
+ #
32
+ def unsubscribe(*names, &callback)
33
+ names = :all unless names && !names.empty?
34
+ Array(names).each do |name|
35
+ if name == :all
36
+ message_emitter_subscriptions.keys
37
+ else
38
+ Array(message_emitter_subscriptions_message_name_key(name))
39
+ end.each do |key|
40
+ message_emitter_subscriptions[key].delete_if do |block|
41
+ !block_given? || callback == block
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ # Emit a message to message subscribers
48
+ #
49
+ # param name [String,Symbol] the event name
50
+ # param payload [Object] the event object to emit
51
+ #
52
+ # @return [void]
53
+ #
54
+ # @api private
55
+ def emit_message(name, payload)
56
+ message_emitter_subscriptions[:all].each { |cb| safe_yield(cb, payload) }
57
+ message_emitter_subscriptions[name].each { |cb| safe_yield(cb, payload) } if name
58
+ end
59
+
60
+ private
61
+ def message_emitter_subscriptions
62
+ @message_emitter_subscriptions ||= Hash.new { |hash, key| hash[key] = [] }
63
+ end
64
+
65
+ def message_emitter_subscriptions_message_name_key(name)
66
+ if name == :all
67
+ :all
68
+ else
69
+ message_emitter_subscriptions_coerce_message_key(name)
70
+ end
71
+ end
72
+
73
+ # this method can be overwritten easily to enforce use of set key types§
74
+ def message_emitter_subscriptions_coerce_message_key(name)
75
+ name.to_s
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,14 @@
1
+ require 'msgpack'
2
+
3
+ module Ably::Modules
4
+ # MessagePack module adds #to_msgpack to the class on the assumption that the class
5
+ # supports the method #as_json
6
+ #
7
+ module MessagePack
8
+ # Generate a packed MsgPack version of this object based on the JSON representation.
9
+ # Keys thus use mixedCase syntax as expected by the Realtime API
10
+ def to_msgpack(pk = nil)
11
+ as_json.to_msgpack(pk)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,84 @@
1
+ require 'base64'
2
+ require 'ably/modules/conversions'
3
+ require 'ably/modules/message_pack'
4
+
5
+ module Ably::Modules
6
+ # Common model functionality shared across many {Ably::Models}
7
+ module ModelCommon
8
+ include Conversions
9
+ include MessagePack
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ # Provide a normal Hash accessor to the underlying raw message object
16
+ #
17
+ # @return [Object]
18
+ #
19
+ def [](key)
20
+ attributes[key]
21
+ end
22
+
23
+ def ==(other)
24
+ other.kind_of?(self.class) &&
25
+ attributes == other.attributes
26
+ end
27
+
28
+ # Return a JSON ready object from the underlying #attributes using Ably naming conventions for keys
29
+ #
30
+ # @return [Hash]
31
+ #
32
+ def as_json(*args)
33
+ attributes.as_json.reject { |key, val| val.nil? }
34
+ end
35
+
36
+ # Stringify the JSON representation of this object from the underlying #attributes
37
+ #
38
+ # @return [String]
39
+ #
40
+ def to_json(*args)
41
+ as_json.to_json(*args)
42
+ end
43
+
44
+ # @!attribute [r] hash
45
+ # @return [Integer] Compute a hash-code for this hash. Two hashes with the same content will have the same hash code
46
+ def hash
47
+ attributes.hash
48
+ end
49
+
50
+ def to_s
51
+ representation = attributes.map do |key, val|
52
+ if val.nil?
53
+ nil
54
+ else
55
+ val_str = val.to_s
56
+ val_str = "#{val_str[0...80]}..." if val_str.length > 80
57
+ "#{key}=#{val_str}"
58
+ end
59
+ end
60
+ "<#{self.class.name}: #{representation.compact.join(', ')}>"
61
+ end
62
+
63
+ module ClassMethods
64
+ # Return a new instance of this object using the provided JSON-like object or JSON string
65
+ # @param json_like_object [Hash, String] JSON-like object or JSON string
66
+ # @return a new instance to this object
67
+ def from_json(json_like_object)
68
+ if json_like_object.kind_of?(String)
69
+ new(JSON.parse(json_like_object))
70
+ else
71
+ new(json_like_object)
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+ def ensure_utf8_string_for(attribute, value)
78
+ if value
79
+ raise ArgumentError, "#{attribute} must be a String" unless value.kind_of?(String)
80
+ raise ArgumentError, "#{attribute} cannot use ASCII_8BIT encoding, please use UTF_8 encoding" unless value.encoding == Encoding::UTF_8
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,83 @@
1
+ require 'eventmachine'
2
+
3
+ module Ably::Modules
4
+ # SafeDeferrable module provides an EventMachine::Deferrable interface to the object it is included in
5
+ # and is safe to use for for public interfaces of this client library.
6
+ # Any exceptions raised in the success or failure callbacks are caught and logged to #logger
7
+ #
8
+ # An exception in a callback provided by a developer should not break this client library
9
+ # and stop further execution of code.
10
+ #
11
+ # @note this Module requires that the method #logger is available
12
+ #
13
+ # See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable
14
+ #
15
+ module SafeDeferrable
16
+ include EventMachine::Deferrable
17
+
18
+ # Specify a block to be executed if and when the Deferrable object receives
19
+ # a status of :succeeded.
20
+ # See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#callback-instance_method
21
+ #
22
+ # @return [void]
23
+ #
24
+ def callback(&block)
25
+ super do |*args|
26
+ safe_deferrable_block(*args, &block)
27
+ end
28
+ end
29
+
30
+ # Specify a block to be executed if and when the Deferrable object receives
31
+ # a status of :failed.
32
+ # See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#errback-instance_method
33
+ #
34
+ # @return [void]
35
+ #
36
+ def errback(&block)
37
+ super do |*args|
38
+ safe_deferrable_block(*args, &block)
39
+ end
40
+ end
41
+
42
+ # Mark the Deferrable as succeeded and trigger all success callbacks.
43
+ # See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#succeed-instance_method
44
+ #
45
+ # @return [void]
46
+ #
47
+ def succeed(*args)
48
+ super(*args)
49
+ end
50
+
51
+ # Mark the Deferrable as failed and trigger all error callbacks.
52
+ # See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#fail-instance_method
53
+ #
54
+ # @return [void]
55
+ #
56
+ def fail(*args)
57
+ super(*args)
58
+ end
59
+
60
+ private
61
+ def safe_deferrable_block(*args)
62
+ yield(*args)
63
+ rescue StandardError => e
64
+ message = "An exception in a Deferrable callback was caught. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
65
+ if defined?(:logger) && logger.respond_to?(:error)
66
+ logger.error message
67
+ else
68
+ fallback_logger.error message
69
+ end
70
+ end
71
+
72
+ def fallback_logger
73
+ @fallback_logger ||= ::Logger.new(STDOUT).tap do |logger|
74
+ logger.formatter = lambda do |severity, datetime, progname, msg|
75
+ [
76
+ "#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")} #{::Logger::SEV_LABEL[severity]} #{msg}",
77
+ "Warning: SafeDeferrable expects the method #logger to be defined in the class it is included in, the method was not found in #{self.class}"
78
+ ].join("\n")
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,41 @@
1
+ module Ably::Modules
2
+ # SafeYield provides the method safe_yield that will yield to the consumer
3
+ # who provided a block, however any exceptions will be caught, logged, and
4
+ # operation of the client library will continue.
5
+ #
6
+ # An exception in a callback provided by a developer should not break this client library
7
+ # and stop further execution of code.
8
+ #
9
+ # @note this Module requires that the method #logger is available
10
+ #
11
+ # @api private
12
+ module SafeYield
13
+ private
14
+
15
+ def safe_yield(block, *args)
16
+ block.call(*args)
17
+ rescue StandardError => e
18
+ message = "An exception in an external block was caught. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
19
+ safe_yield_log_error message
20
+ end
21
+
22
+ def safe_yield_log_error(message)
23
+ if defined?(:logger) && logger.respond_to?(:error)
24
+ return logger.error message
25
+ end
26
+ rescue StandardError
27
+ fallback_logger.error message
28
+ end
29
+
30
+ def fallback_logger
31
+ @fallback_logger ||= ::Logger.new(STDOUT).tap do |logger|
32
+ logger.formatter = lambda do |severity, datetime, progname, msg|
33
+ [
34
+ "#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")} #{::Logger::SEV_LABEL[severity]} #{msg}",
35
+ "Warning: SafeYield expects the method #logger to be defined in the class it is included in, the method was not found in #{self.class}"
36
+ ].join("\n")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,181 @@
1
+ module Ably::Modules
2
+ # StateEmitter module adds a set of generic state related methods to a class on the assumption that
3
+ # the instance variable @state is used exclusively, the {Enum} STATE is defined prior to inclusion of this
4
+ # module, and the class is an {EventEmitter}. It then emits state changes.
5
+ #
6
+ # It also ensures the EventEmitter is configured to restrict permitted events to the
7
+ # the available STATEs or EVENTs if defined i.e. if EVENTs includes an additional type such as
8
+ # :update, then it will support all EVENTs being emitted. EVENTs must be a superset of STATEs
9
+ #
10
+ # @note This module requires that the method #logger is defined.
11
+ #
12
+ # @example
13
+ # class Connection
14
+ # include Ably::Modules::EventEmitter
15
+ # extend Ably::Modules::Enum
16
+ # STATE = ruby_enum('STATE',
17
+ # :initialized,
18
+ # :connecting,
19
+ # :connected
20
+ # )
21
+ # include Ably::Modules::StateEmitter
22
+ # end
23
+ #
24
+ # connection = Connection.new
25
+ # connection.state = :connecting # emits :connecting event via EventEmitter, returns STATE.Connecting
26
+ # connection.state?(:connected) # => false
27
+ # connection.connecting? # => true
28
+ # connection.state # => STATE.Connecting
29
+ # connection.state = :invalid # raises an Exception as only a valid state can be defined
30
+ # connection.emit :invalid # raises an Exception as only a valid state can be used for EventEmitter
31
+ # connection.change_state :connected # emits :connected event via EventEmitter, returns STATE.Connected
32
+ # connection.once_or_if(:connected) { puts 'block called once when state is connected or becomes connected' }
33
+ #
34
+ module StateEmitter
35
+ # Current state {Ably::Modules::Enum}
36
+ #
37
+ # @return [Symbol] state
38
+ #
39
+ def state
40
+ STATE(@state)
41
+ end
42
+
43
+ # Evaluates if check_state matches current state
44
+ #
45
+ # @return [Boolean]
46
+ #
47
+ def state?(check_state)
48
+ state == check_state
49
+ end
50
+
51
+ # Set the current state {Ably::Modules::Enum}
52
+ #
53
+ # @return [Symbol] new state
54
+ # @api private
55
+ def state=(new_state, *args)
56
+ if state != new_state
57
+ logger.debug { "#{self.class}: StateEmitter changed from #{state} => #{new_state}" } if respond_to?(:logger, true)
58
+ @state = STATE(new_state)
59
+ emit @state, *args
60
+ end
61
+ end
62
+ alias_method :change_state, :state=
63
+
64
+ # If the current state matches the target_state argument the block is called immediately.
65
+ # Else the block is called once when the target_state is reached.
66
+ #
67
+ # If the option block :else is provided then if any state other than target_state is reached, the :else block is called,
68
+ # however only one of the blocks will ever be called
69
+ #
70
+ # @param [Symbol,Ably::Modules::Enum,Array] target_states a single state or array of states that once met, will fire the success block only once
71
+ # @param [Hash] options
72
+ # @option options [Proc] :else block called once the state has changed to anything but target_state
73
+ #
74
+ # @yield block is called if the state is matched immediately or once when the state is reached
75
+ #
76
+ # @return [void]
77
+ def once_or_if(target_states, options = {}, &block)
78
+ raise ArgumentError, 'Block required' unless block_given?
79
+
80
+ if Array(target_states).any? { |target_state| state == target_state }
81
+ safe_yield block
82
+ else
83
+ failure_block = options.fetch(:else, nil)
84
+ failure_wrapper = nil
85
+
86
+ success_wrapper = lambda do |*args|
87
+ yield
88
+ off(&success_wrapper)
89
+ off(&failure_wrapper) if failure_wrapper
90
+ end
91
+
92
+ failure_wrapper = lambda do |*args|
93
+ failure_block.call(*args)
94
+ off(&success_wrapper)
95
+ off(&failure_wrapper)
96
+ end if failure_block
97
+
98
+ Array(target_states).each do |target_state|
99
+ safe_unsafe_method options[:unsafe], :once, target_state, &success_wrapper
100
+
101
+ safe_unsafe_method options[:unsafe], :once_state_changed do |*args|
102
+ failure_wrapper.call(*args) unless state == target_state
103
+ end if failure_block
104
+ end
105
+ end
106
+ end
107
+
108
+ # Equivalent of {#once_or_if} but any exception raised in a block will bubble up and cause this client library to fail.
109
+ # This method should only be used internally by the client library.
110
+ # @api private
111
+ def unsafe_once_or_if(target_states, options = {}, &block)
112
+ once_or_if(target_states, options.merge(unsafe: true), &block)
113
+ end
114
+
115
+ # Calls the block once when the state changes
116
+ #
117
+ # @yield block is called once the state changes
118
+ # @return [void]
119
+ #
120
+ # @api private
121
+ def once_state_changed(options = {}, &block)
122
+ raise ArgumentError, 'Block required' unless block_given?
123
+
124
+ once_block = lambda do |*args|
125
+ off(*self.class::STATE.map, &once_block)
126
+ yield(*args)
127
+ end
128
+
129
+ safe_unsafe_method options[:unsafe], :once, *self.class::STATE.map, &once_block
130
+ end
131
+
132
+ # Equivalent of {#once_state_changed} but any exception raised in a block will bubble up and cause this client library to fail.
133
+ # This method should only be used internally by the client library.
134
+ # @api private
135
+ def unsafe_once_state_changed(&block)
136
+ once_state_changed(unsafe: true, &block)
137
+ end
138
+
139
+ private
140
+
141
+ # Returns an {Ably::Util::SafeDeferrable} and once the target state is reached, the
142
+ # success block if provided and {Ably::Util::SafeDeferrable#callback} is called.
143
+ # If the state changes to any other state, the {Ably::Util::SafeDeferrable#errback} is called.
144
+ #
145
+ def deferrable_for_state_change_to(target_state)
146
+ Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
147
+ fail_proc = lambda do |state_change|
148
+ deferrable.fail state_change.reason
149
+ end
150
+ once_or_if(target_state, else: fail_proc) do
151
+ yield self if block_given?
152
+ deferrable.succeed self
153
+ end
154
+ end
155
+ end
156
+
157
+ def self.included(klass)
158
+ klass.configure_event_emitter coerce_into: lambda { |event|
159
+ # Special case allows EVENT instead of STATE to be emitted
160
+ # Relies on the assumption that EVENT is a superset of STATE
161
+ if klass.const_defined?(:EVENT)
162
+ klass::EVENT(event)
163
+ else
164
+ klass::STATE(event)
165
+ end
166
+ }
167
+
168
+ klass::STATE.each do |state_predicate|
169
+ klass.instance_eval do
170
+ define_method("#{state_predicate.to_sym}?") do
171
+ state?(state_predicate)
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def safe_unsafe_method(unsafe, method_name, *args, &block)
178
+ public_send("#{'unsafe_' if unsafe}#{method_name}", *args, &block)
179
+ end
180
+ end
181
+ end