From c9939818b832d7d2c0ac8f41936ea093f2e30040 Mon Sep 17 00:00:00 2001 From: matt423 Date: Fri, 27 Feb 2026 17:13:42 +0000 Subject: [PATCH 1/8] Add update_message support for REST and Realtime channels Implements message update functionality following the Ably specification (RSL15, RTL32, TM2, TM5, MOP, UDR). Bumps protocol version from 2 to 5 to enable ACK responses with publish result serials. - Add Message ACTION enum (message_create, message_update) and new accessors (action, serial, version, created_at, updated_at) - Add MessageOperation and UpdateDeleteResult model types - Add REST channel#update_message via PATCH /channels/{name}/messages/{serial} - Add Realtime channel#update_message via MESSAGE ProtocolMessage - Add ProtocolMessage#res accessor for protocol v5 ACK results - Update ACK handling to route UpdateDeleteResult to update deferrables - Add REST client#patch convenience method --- lib/ably/models/message.rb | 65 +++++++- lib/ably/models/message_operation.rb | 53 +++++++ lib/ably/models/protocol_message.rb | 13 ++ lib/ably/models/update_delete_result.rb | 32 ++++ lib/ably/realtime/channel.rb | 68 ++++++++ .../client/incoming_message_dispatcher.rb | 21 ++- lib/ably/rest/channel.rb | 46 ++++++ lib/ably/rest/client.rb | 9 ++ lib/ably/version.rb | 2 +- spec/acceptance/realtime/connection_spec.rb | 2 +- spec/unit/models/message_operation_spec.rb | 67 ++++++++ spec/unit/models/message_spec.rb | 149 ++++++++++++++++++ spec/unit/models/protocol_message_spec.rb | 40 +++++ spec/unit/models/update_delete_result_spec.rb | 52 ++++++ .../incoming_message_dispatcher_spec.rb | 146 +++++++++++++++++ spec/unit/rest/channel_spec.rb | 117 ++++++++++++++ 16 files changed, 875 insertions(+), 7 deletions(-) create mode 100644 lib/ably/models/message_operation.rb create mode 100644 lib/ably/models/update_delete_result.rb create mode 100644 spec/unit/models/message_operation_spec.rb create mode 100644 spec/unit/models/update_delete_result_spec.rb diff --git a/lib/ably/models/message.rb b/lib/ably/models/message.rb index 5c5817b06..02350b71a 100644 --- a/lib/ably/models/message.rb +++ b/lib/ably/models/message.rb @@ -26,6 +26,16 @@ class Message include Ably::Modules::Encodeable include Ably::Modules::ModelCommon include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime) + extend Ably::Modules::Enum + + # Describes the possible actions for a message. + # + # @spec TM5 + # + ACTION = ruby_enum('ACTION', + :message_create, # 0 + :message_update, # 1 + ) # Statically register a default set of encoders for this class Ably::Models::MessageEncoders.register_default_encoders self @@ -129,10 +139,63 @@ def timestamp end end + # The action type of this message. + # + # @spec TM2j + # + # @return [ACTION, nil] + # + def action + ACTION(attributes[:action]) if attributes[:action] + end + + # An opaque string that uniquely identifies the message within a channel. + # + # @spec TM2r + # + # @return [String, nil] + # + def serial + attributes[:serial] + end + + # Version information about this message. + # + # @spec TM2s + # + # @return [Hash, nil] + # + def version + attributes[:version] + end + + # Timestamp of when the message was created. + # + # @return [Time, nil] + # + def created_at + as_time_from_epoch(attributes[:created_at]) if attributes[:created_at] + end + + # Timestamp of when the message was last updated. + # + # @return [Time, nil] + # + def updated_at + as_time_from_epoch(attributes[:updated_at]) if attributes[:updated_at] + end + def attributes @attributes end + # Return a JSON ready object from the underlying #attributes using Ably naming conventions for keys + def as_json(*args) + attributes.dup.tap do |message| + message['action'] = action.to_i if attributes[:action] + end.as_json.reject { |key, val| val.nil? } + end + def to_json(*args) as_json(*args).tap do |message| decode_binary_data_before_to_json message @@ -212,7 +275,7 @@ def raw_hash_object end def set_attributes_object(new_attributes) - @attributes = IdiomaticRubyWrapper(new_attributes.clone, stop_at: [:data, :extras]) + @attributes = IdiomaticRubyWrapper(new_attributes.clone, stop_at: [:data, :extras, :version]) end def logger diff --git a/lib/ably/models/message_operation.rb b/lib/ably/models/message_operation.rb new file mode 100644 index 000000000..3a1f2de2a --- /dev/null +++ b/lib/ably/models/message_operation.rb @@ -0,0 +1,53 @@ +module Ably::Models + # Represents the operation metadata for update, delete, or append message operations. + # + # @spec MOP + # + class MessageOperation + include Ably::Modules::ModelCommon + + # @param attributes [Hash] + # @option attributes [String] :client_id The client ID of the user performing the operation (MOP2a) + # @option attributes [String] :description An optional human-readable description of the operation (MOP2b) + # @option attributes [Hash] :metadata Arbitrary key-value metadata for the operation (MOP2c) + # + def initialize(attributes = {}) + @hash_object = IdiomaticRubyWrapper(attributes || {}, stop_at: [:metadata]) + @hash_object.freeze + end + + # The client ID of the user performing the operation. + # + # @spec MOP2a + # + # @return [String, nil] + # + def client_id + attributes[:client_id] + end + + # An optional human-readable description of the operation. + # + # @spec MOP2b + # + # @return [String, nil] + # + def description + attributes[:description] + end + + # Arbitrary key-value metadata for the operation. + # + # @spec MOP2c + # + # @return [Hash, nil] + # + def metadata + attributes[:metadata] + end + + def attributes + @hash_object + end + end +end diff --git a/lib/ably/models/protocol_message.rb b/lib/ably/models/protocol_message.rb index 19ac1662e..a8d304c42 100644 --- a/lib/ably/models/protocol_message.rb +++ b/lib/ably/models/protocol_message.rb @@ -171,6 +171,19 @@ def params @params ||= attributes[:params].to_h end + # The res field from ACK protocol messages (protocol v5+), containing publish results. + # Each entry corresponds to an ACK'd ProtocolMessage and contains a serials array + # with one entry per message in that ProtocolMessage. + # + # @spec TR4s + # + # @return [Array, nil] + # + # @api private + def res + attributes[:res] + end + def flags Integer(attributes[:flags]) rescue TypeError diff --git a/lib/ably/models/update_delete_result.rb b/lib/ably/models/update_delete_result.rb new file mode 100644 index 000000000..3ca109490 --- /dev/null +++ b/lib/ably/models/update_delete_result.rb @@ -0,0 +1,32 @@ +module Ably::Models + # Contains the result of an update or delete message operation. + # + # @spec UDR + # + class UpdateDeleteResult + include Ably::Modules::ModelCommon + + # @param attributes [Hash] + # @option attributes [String, nil] :version_serial The new version serial string of the updated or deleted message. + # Will be nil if the message was superseded by a subsequent update before it could be published. + # + def initialize(attributes = {}) + @hash_object = IdiomaticRubyWrapper(attributes || {}) + @hash_object.freeze + end + + # The version serial of the updated or deleted message, or nil if superseded. + # + # @spec UDR2a + # + # @return [String, nil] + # + def version_serial + attributes[:version_serial] + end + + def attributes + @hash_object + end + end +end diff --git a/lib/ably/realtime/channel.rb b/lib/ably/realtime/channel.rb index af1772c0f..65d359fbe 100644 --- a/lib/ably/realtime/channel.rb +++ b/lib/ably/realtime/channel.rb @@ -213,6 +213,74 @@ def publish(name, data = nil, attributes = {}, &success_block) end end + # Updates a previously published message on the channel. A callback may optionally be passed in to this + # call to be notified of success or failure of the operation. + # + # @spec RTL32 + # + # @param message [Ably::Models::Message, Hash] A Message object or Hash containing a populated :serial field + # and the fields to update. + # @param operation [Hash, Ably::Models::MessageOperation, nil] Optional operation metadata. + # @param params [Hash, nil] Optional parameters sent as part of the protocol message. + # + # @yield [Ably::Models::UpdateDeleteResult] On success, calls the block with the result containing version_serial. + # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks + # + def update_message(message, operation = nil, params = {}, &success_block) + if suspended? || failed? + error = Ably::Exceptions::ChannelInactive.new("Cannot update messages on a channel in state #{state}") + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + if !connection.can_publish_messages? + error = Ably::Exceptions::MessageQueueingDisabled.new( + "Message cannot be updated. Client is not allowed to queue messages when connection is in state #{connection.state}" + ) + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + message = Ably::Models::Message(message) + + unless message.serial + error = Ably::Exceptions::InvalidRequest.new('Message serial is required for update operations. Ensure the message has a serial field.') + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + # RTL32c - Do not mutate the user-supplied message; build a new one + update_attrs = message.as_json + update_attrs['action'] = Ably::Models::Message::ACTION.MessageUpdate.to_i + + if operation + op_hash = operation.respond_to?(:as_json) ? operation.as_json : operation + update_attrs['version'] = op_hash + end + + updated_message = Ably::Models::Message.new(update_attrs) + updated_message.encode(client.encoders, options) do |encode_error, error_message| + client.logger.error error_message + end + + pm_params = { action: Ably::Models::ProtocolMessage::ACTION.Message.to_i, channel: name, messages: [updated_message] } + pm_params[:params] = params.transform_values(&:to_s) if params && !params.empty? + + connection.send_protocol_message(pm_params) + + # Wrap the inner message deferrable to return UpdateDeleteResult + Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| + updated_message.callback do |result| + if result.is_a?(Ably::Models::UpdateDeleteResult) + deferrable.succeed result + else + deferrable.succeed Ably::Models::UpdateDeleteResult.new(version_serial: nil) + end + end + updated_message.errback do |error| + deferrable.fail error + end + deferrable.callback(&success_block) if block_given? + end + end + # Registers a listener for messages on this channel. The caller supplies a listener function, which is called # each time one or more messages arrives on the channel. A callback may optionally be passed in to this call # to be notified of success or failure of the channel {Ably::Realtime::Channel#attach} operation. diff --git a/lib/ably/realtime/client/incoming_message_dispatcher.rb b/lib/ably/realtime/client/incoming_message_dispatcher.rb index 435d70660..2ad53f4b5 100644 --- a/lib/ably/realtime/client/incoming_message_dispatcher.rb +++ b/lib/ably/realtime/client/incoming_message_dispatcher.rb @@ -178,9 +178,12 @@ def process_connected_update_message(protocol_message) end def ack_pending_queue_for_message_serial(ack_protocol_message) + res_index = 0 drop_pending_queue_from_ack(ack_protocol_message) do |protocol_message| - ack_messages protocol_message.messages + publish_result = ack_protocol_message.res[res_index] if ack_protocol_message.res + ack_messages protocol_message.messages, publish_result ack_messages protocol_message.presence + res_index += 1 end end @@ -191,10 +194,20 @@ def nack_pending_queue_for_message_serial(nack_protocol_message) end end - def ack_messages(messages) - messages.each do |message| + def ack_messages(messages, publish_result = nil) + messages.each_with_index do |message, index| logger.debug { "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}" } - message.succeed message + if publish_result && message.respond_to?(:action) && message.action && + message.action.match_any?(Ably::Models::Message::ACTION.MessageUpdate) + serials = publish_result.is_a?(Hash) ? + (publish_result['serials'] || publish_result[:serials]) : + publish_result[:serials] + version_serial = serials[index] if serials + result = Ably::Models::UpdateDeleteResult.new(version_serial: version_serial) + message.succeed result + else + message.succeed message + end end end diff --git a/lib/ably/rest/channel.rb b/lib/ably/rest/channel.rb index a8bcfb83a..586ebedca 100644 --- a/lib/ably/rest/channel.rb +++ b/lib/ably/rest/channel.rb @@ -115,6 +115,52 @@ def publish(name, data = nil, attributes = {}) [201, 204].include?(response.status) end + # Updates a previously published message on the channel. Uses patch semantics: non-null fields + # in the provided message will replace the corresponding fields in the existing message, while + # null fields will be left unchanged. + # + # @spec RSL15 + # + # @param message [Ably::Models::Message, Hash] A Message object or Hash containing a populated :serial field + # and the fields to update. + # @param operation [Hash, Ably::Models::MessageOperation, nil] Optional operation metadata containing + # :description and/or :metadata fields. + # @param params [Hash, nil] Optional parameters sent as part of the query string. + # + # @return [Ably::Models::UpdateDeleteResult] The result containing the version_serial. + # + def update_message(message, operation = nil, params = {}) + message = Ably::Models::Message(message) + + raise Ably::Exceptions::InvalidRequest.new( + 'Message serial is required for update operations. Ensure the message has a serial field.' + ) unless message.serial + + # RSL15c - Do not mutate the user-supplied message; build a new one + update_attrs = message.as_json + update_attrs['action'] = Ably::Models::Message::ACTION.MessageUpdate.to_i + + if operation + op_hash = operation.respond_to?(:as_json) ? operation.as_json : operation + update_attrs['version'] = op_hash + end + + updated_message = Ably::Models::Message.new(update_attrs) + updated_message.encode client.encoders, options + + payload = updated_message.as_json + serial = message.serial + + request_options = params && !params.empty? ? { qs_params: params } : {} + response = client.patch( + "#{base_path}/messages/#{URI.encode_www_form_component(serial)}", + payload, + request_options + ) + + Ably::Models::UpdateDeleteResult.new(response.body || {}) + end + # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical {Ably::Models::Message} # objects for the channel. If the channel is configured to persist messages, then messages can be retrieved from # history for up to 72 hours in the past. If not, messages can only be retrieved from history for up to two minutes in the past. diff --git a/lib/ably/rest/client.rb b/lib/ably/rest/client.rb index 3dbda2dfb..88c30edd3 100644 --- a/lib/ably/rest/client.rb +++ b/lib/ably/rest/client.rb @@ -351,6 +351,15 @@ def put(path, params, options = {}) raw_request(:put, path, params, options) end + # Perform an HTTP PATCH request to the API using configured authentication + # + # @return [Faraday::Response] + # + # @api private + def patch(path, params, options = {}) + raw_request(:patch, path, params, options) + end + # Perform an HTTP DELETE request to the API using configured authentication # # @return [Faraday::Response] diff --git a/lib/ably/version.rb b/lib/ably/version.rb index 64eef5be7..08aaddc87 100644 --- a/lib/ably/version.rb +++ b/lib/ably/version.rb @@ -3,5 +3,5 @@ module Ably # The level of compatibility with the Ably service that this SDK supports. # Also referred to as the 'wire protocol version'. # spec : CSV2 - PROTOCOL_VERSION = '2' + PROTOCOL_VERSION = '5' end diff --git a/spec/acceptance/realtime/connection_spec.rb b/spec/acceptance/realtime/connection_spec.rb index 36ca720fd..ca042f296 100644 --- a/spec/acceptance/realtime/connection_spec.rb +++ b/spec/acceptance/realtime/connection_spec.rb @@ -1996,7 +1996,7 @@ def self.available_states it 'sends the protocol version param v (#G4, #RTN2f)' do expect(EventMachine).to receive(:connect) do |host, port, transport, object, url| uri = URI.parse(url) - expect(CGI::parse(uri.query)['v'][0]).to eql('2') + expect(CGI::parse(uri.query)['v'][0]).to eql('5') stop_reactor end client diff --git a/spec/unit/models/message_operation_spec.rb b/spec/unit/models/message_operation_spec.rb new file mode 100644 index 000000000..85acc8df5 --- /dev/null +++ b/spec/unit/models/message_operation_spec.rb @@ -0,0 +1,67 @@ +# encoding: utf-8 +require 'spec_helper' + +describe Ably::Models::MessageOperation do + subject { Ably::Models::MessageOperation } + + context '#client_id (#MOP2a)' do + let(:model) { subject.new(client_id: 'user123') } + + it 'returns the client_id' do + expect(model.client_id).to eql('user123') + end + end + + context '#description (#MOP2b)' do + let(:model) { subject.new(description: 'Edited for clarity') } + + it 'returns the description' do + expect(model.description).to eql('Edited for clarity') + end + end + + context '#metadata (#MOP2c)' do + let(:model) { subject.new(metadata: { 'reason' => 'typo' }) } + + it 'returns the metadata hash' do + expect(model.metadata).to eq({ 'reason' => 'typo' }) + end + end + + context 'when empty' do + let(:model) { subject.new({}) } + + it 'returns nil for all fields' do + expect(model.client_id).to be_nil + expect(model.description).to be_nil + expect(model.metadata).to be_nil + end + end + + context 'with camelCase keys from wire' do + let(:model) { subject.new('clientId' => 'user456') } + + it 'converts to snake_case access' do + expect(model.client_id).to eql('user456') + end + end + + context '#attributes' do + let(:model) { subject.new(description: 'test') } + + it 'prevents modification' do + expect { model.attributes[:description] = 'changed' }.to raise_error(/can't modify frozen|FrozenError/) + end + end + + context '#as_json' do + let(:model) { subject.new(client_id: 'user', description: 'edit') } + + it 'returns a hash suitable for JSON serialization' do + json = model.as_json + expect(json).to be_a(Hash) + expect(json['clientId']).to eql('user') + expect(json['description']).to eql('edit') + end + end +end diff --git a/spec/unit/models/message_spec.rb b/spec/unit/models/message_spec.rb index a2939920f..c5e0f1390 100644 --- a/spec/unit/models/message_spec.rb +++ b/spec/unit/models/message_spec.rb @@ -644,4 +644,153 @@ end end end + + context '#action (#TM2j)' do + context 'when action is present' do + let(:model) { subject.new({ action: 1 }) } + + it 'returns the action as an ACTION enum' do + expect(model.action).to eq(Ably::Models::Message::ACTION.MessageUpdate) + end + + it 'can be compared with a symbol' do + expect(model.action).to eq(:message_update) + end + + it 'can be compared with an integer' do + expect(model.action).to eq(1) + end + end + + context 'when action is not present' do + let(:model) { subject.new({}) } + + it 'returns nil' do + expect(model.action).to be_nil + end + end + + context 'ACTION enum values (#TM5)' do + it 'has message_create as 0' do + expect(Ably::Models::Message::ACTION.MessageCreate.to_i).to eq(0) + end + + it 'has message_update as 1' do + expect(Ably::Models::Message::ACTION.MessageUpdate.to_i).to eq(1) + end + end + end + + context '#serial (#TM2r)' do + let(:serial_value) { 'msg-serial-001' } + let(:model) { subject.new({ serial: serial_value }) } + + it 'returns the serial attribute' do + expect(model.serial).to eql(serial_value) + end + + context 'when not present' do + let(:model) { subject.new({}) } + + it 'returns nil' do + expect(model.serial).to be_nil + end + end + end + + context '#version (#TM2s)' do + let(:version_data) { { 'serial' => 'v1', 'timestamp' => 1234567890 } } + let(:model) { subject.new({ version: version_data }) } + + it 'returns the version attribute' do + expect(model.version).to eq(version_data) + end + + context 'when not present' do + let(:model) { subject.new({}) } + + it 'returns nil' do + expect(model.version).to be_nil + end + end + end + + context '#created_at' do + let(:time_ms) { (Time.now.to_f * 1000).to_i } + let(:model) { subject.new({ created_at: time_ms }) } + + it 'returns a Time object' do + expect(model.created_at).to be_a(Time) + end + + context 'when not present' do + let(:model) { subject.new({}) } + + it 'returns nil' do + expect(model.created_at).to be_nil + end + end + end + + context '#updated_at' do + let(:time_ms) { (Time.now.to_f * 1000).to_i } + let(:model) { subject.new({ updated_at: time_ms }) } + + it 'returns a Time object' do + expect(model.updated_at).to be_a(Time) + end + + context 'when not present' do + let(:model) { subject.new({}) } + + it 'returns nil' do + expect(model.updated_at).to be_nil + end + end + end + + context '#as_json' do + context 'with action' do + let(:model) { subject.new({ name: 'test', action: 1 }) } + + it 'converts action to integer' do + expect(model.as_json['action']).to eq(1) + end + + it 'includes name' do + expect(model.as_json['name']).to eq('test') + end + end + + context 'without action' do + let(:model) { subject.new({ name: 'test' }) } + + it 'does not include action key' do + expect(model.as_json).not_to have_key('action') + end + end + + context 'with serial and version' do + let(:model) { subject.new({ serial: 'abc', version: { 'description' => 'edit' } }) } + + it 'includes serial' do + expect(model.as_json['serial']).to eq('abc') + end + + it 'includes version' do + expect(model.as_json['version']).to eq({ 'description' => 'edit' }) + end + end + + context 'excludes nil values' do + let(:model) { subject.new({ name: 'test' }) } + + it 'does not include nil attributes' do + json = model.as_json + expect(json).not_to have_key('serial') + expect(json).not_to have_key('version') + expect(json).not_to have_key('encoding') + end + end + end end diff --git a/spec/unit/models/protocol_message_spec.rb b/spec/unit/models/protocol_message_spec.rb index 32c74cbc3..67ab2eabf 100644 --- a/spec/unit/models/protocol_message_spec.rb +++ b/spec/unit/models/protocol_message_spec.rb @@ -215,6 +215,46 @@ def new_protocol_message(options) end end + context '#res (#TR4s)' do + context 'when present' do + let(:res_data) { [{ 'serials' => ['serial-001', 'serial-002'] }] } + let(:protocol_message) { new_protocol_message(res: res_data) } + + it 'returns the res array' do + expect(protocol_message.res).to be_a(Array) + expect(protocol_message.res.length).to eql(1) + end + + it 'contains publish result entries with serials' do + entry = protocol_message.res[0] + expect(entry['serials']).to eq(['serial-001', 'serial-002']) + end + end + + context 'when absent' do + let(:protocol_message) { new_protocol_message({}) } + + it 'returns nil' do + expect(protocol_message.res).to be_nil + end + end + + context 'with multiple entries' do + let(:res_data) do + [ + { 'serials' => ['serial-a'] }, + { 'serials' => ['serial-b', 'serial-c'] } + ] + end + let(:protocol_message) { new_protocol_message(res: res_data) } + + it 'returns all entries' do + expect(protocol_message.res.length).to eql(2) + expect(protocol_message.res[1]['serials']).to eq(['serial-b', 'serial-c']) + end + end + end + context '#params (#RTL4k1)' do let(:params) do { foo: :bar } diff --git a/spec/unit/models/update_delete_result_spec.rb b/spec/unit/models/update_delete_result_spec.rb new file mode 100644 index 000000000..cfd13dbb1 --- /dev/null +++ b/spec/unit/models/update_delete_result_spec.rb @@ -0,0 +1,52 @@ +# encoding: utf-8 +require 'spec_helper' + +describe Ably::Models::UpdateDeleteResult do + subject { Ably::Models::UpdateDeleteResult } + + context '#version_serial (#UDR2a)' do + context 'when present' do + let(:model) { subject.new(version_serial: 'v1-serial') } + + it 'returns the version serial' do + expect(model.version_serial).to eql('v1-serial') + end + end + + context 'when nil' do + let(:model) { subject.new(version_serial: nil) } + + it 'returns nil' do + expect(model.version_serial).to be_nil + end + end + + context 'when not provided' do + let(:model) { subject.new({}) } + + it 'returns nil' do + expect(model.version_serial).to be_nil + end + end + end + + context 'with camelCase keys from wire' do + let(:model) { subject.new('versionSerial' => 'v1-serial') } + + it 'converts to snake_case access' do + expect(model.version_serial).to eql('v1-serial') + end + end + + context '#attributes' do + let(:model) { subject.new(version_serial: 'v1') } + + it 'returns the underlying attributes' do + expect(model.attributes).to be_a(Ably::Models::IdiomaticRubyWrapper) + end + + it 'prevents modification' do + expect { model.attributes[:version_serial] = 'changed' }.to raise_error(/can't modify frozen|FrozenError/) + end + end +end diff --git a/spec/unit/realtime/incoming_message_dispatcher_spec.rb b/spec/unit/realtime/incoming_message_dispatcher_spec.rb index 760d1e1c7..408109ce8 100644 --- a/spec/unit/realtime/incoming_message_dispatcher_spec.rb +++ b/spec/unit/realtime/incoming_message_dispatcher_spec.rb @@ -33,4 +33,150 @@ msgbus.publish :protocol_message, Ably::Models::ProtocolMessage.new(:action => :attached, channel: 'unknown') end end + + context '#ack_messages' do + let(:dispatcher) { subject } + let(:logger) { instance_double('Logger') } + + before do + allow(dispatcher).to receive(:logger).and_return(logger) + allow(logger).to receive(:debug) + end + + context 'with a regular message (no action)' do + let(:message) do + msg = Ably::Models::Message.new(name: 'test', data: 'hello') + # SafeDeferrable is included when Realtime is loaded + msg + end + + it 'succeeds the message with itself' do + succeeded = false + message.callback do |result| + expect(result).to be(message) + succeeded = true + end + + dispatcher.send(:ack_messages, [message]) + expect(succeeded).to be(true) + end + end + + context 'with a MESSAGE_UPDATE action and publish_result' do + let(:message) do + Ably::Models::Message.new( + name: 'test', + data: 'updated', + serial: 'msg-serial-001', + action: Ably::Models::Message::ACTION.MessageUpdate.to_i + ) + end + let(:publish_result) { { 'serials' => ['version-serial-abc'] } } + + it 'succeeds with an UpdateDeleteResult' do + succeeded = false + message.callback do |result| + expect(result).to be_a(Ably::Models::UpdateDeleteResult) + expect(result.version_serial).to eql('version-serial-abc') + succeeded = true + end + + dispatcher.send(:ack_messages, [message], publish_result) + expect(succeeded).to be(true) + end + end + + context 'with a MESSAGE_UPDATE action but no publish_result' do + let(:message) do + Ably::Models::Message.new( + name: 'test', + serial: 'msg-serial-001', + action: Ably::Models::Message::ACTION.MessageUpdate.to_i + ) + end + + it 'succeeds the message with itself (fallback)' do + succeeded = false + message.callback do |result| + expect(result).to be(message) + succeeded = true + end + + dispatcher.send(:ack_messages, [message]) + expect(succeeded).to be(true) + end + end + + context 'with multiple update messages and a publish_result with multiple serials' do + let(:message1) do + Ably::Models::Message.new( + name: 'test1', + serial: 'serial-1', + action: Ably::Models::Message::ACTION.MessageUpdate.to_i + ) + end + let(:message2) do + Ably::Models::Message.new( + name: 'test2', + serial: 'serial-2', + action: Ably::Models::Message::ACTION.MessageUpdate.to_i + ) + end + let(:publish_result) { { 'serials' => ['vs-aaa', 'vs-bbb'] } } + + it 'assigns the correct version serial to each message by index' do + results = [] + message1.callback { |r| results << r } + message2.callback { |r| results << r } + + dispatcher.send(:ack_messages, [message1, message2], publish_result) + + expect(results.length).to eql(2) + expect(results[0]).to be_a(Ably::Models::UpdateDeleteResult) + expect(results[0].version_serial).to eql('vs-aaa') + expect(results[1]).to be_a(Ably::Models::UpdateDeleteResult) + expect(results[1].version_serial).to eql('vs-bbb') + end + end + + context 'with symbol-keyed publish_result' do + let(:message) do + Ably::Models::Message.new( + name: 'test', + serial: 'msg-serial-001', + action: Ably::Models::Message::ACTION.MessageUpdate.to_i + ) + end + let(:publish_result) { { serials: ['vs-sym'] } } + + it 'handles symbol keys for serials' do + succeeded = false + message.callback do |result| + expect(result).to be_a(Ably::Models::UpdateDeleteResult) + expect(result.version_serial).to eql('vs-sym') + succeeded = true + end + + dispatcher.send(:ack_messages, [message], publish_result) + expect(succeeded).to be(true) + end + end + + context 'with presence messages' do + let(:presence_message) do + Ably::Models::PresenceMessage.new(action: :enter, client_id: 'user1') + end + + it 'succeeds with the message itself (no publish_result handling)' do + succeeded = false + presence_message.callback do |result| + expect(result).to be(presence_message) + succeeded = true + end + + dispatcher.send(:ack_messages, [presence_message]) + expect(succeeded).to be(true) + end + end + end end diff --git a/spec/unit/rest/channel_spec.rb b/spec/unit/rest/channel_spec.rb index 39703be2e..3cd8a6893 100644 --- a/spec/unit/rest/channel_spec.rb +++ b/spec/unit/rest/channel_spec.rb @@ -170,4 +170,121 @@ end end end + + describe '#update_message (#RSL15)' do + let(:serial) { 'msg-serial-001' } + let(:patch_response) { instance_double('Faraday::Response', status: 200, body: { 'versionSerial' => 'v1-serial' }) } + let(:client) do + instance_double( + 'Ably::Rest::Client', + encoders: [], + post: instance_double('Faraday::Response', status: 201), + patch: patch_response, + idempotent_rest_publishing: false, + max_message_size: max_message_size + ) + end + + context 'with a valid message containing serial' do + let(:message) { Ably::Models::Message.new(name: 'test', data: 'hello', serial: serial) } + + it 'sends a PATCH request' do + expect(client).to receive(:patch).with( + "/channels/#{channel_name}/messages/#{serial}", + hash_including('action' => 1), + {} + ).and_return(patch_response) + + subject.update_message(message) + end + + it 'returns an UpdateDeleteResult' do + result = subject.update_message(message) + expect(result).to be_a(Ably::Models::UpdateDeleteResult) + expect(result.version_serial).to eql('v1-serial') + end + + it 'sets action to MESSAGE_UPDATE' do + expect(client).to receive(:patch) do |_path, payload, _opts| + expect(payload['action']).to eq(Ably::Models::Message::ACTION.MessageUpdate.to_i) + patch_response + end + + subject.update_message(message) + end + end + + context 'with an operation parameter' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + let(:operation) { { description: 'Fixed typo', metadata: { 'reason' => 'correction' } } } + + it 'includes the operation as version in the payload' do + expect(client).to receive(:patch) do |_path, payload, _opts| + expect(payload['version']).to eq({ 'description' => 'Fixed typo', 'metadata' => { 'reason' => 'correction' } }) + patch_response + end + + subject.update_message(message, operation) + end + end + + context 'with a MessageOperation object' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + let(:operation) { Ably::Models::MessageOperation.new(description: 'Fixed typo') } + + it 'serializes the operation via as_json' do + expect(client).to receive(:patch) do |_path, payload, _opts| + expect(payload['version']).to be_a(Hash) + expect(payload['version']['description']).to eq('Fixed typo') + patch_response + end + + subject.update_message(message, operation) + end + end + + context 'without serial (#RSL15a)' do + let(:message) { Ably::Models::Message.new(name: 'test', data: 'hello') } + + it 'raises an InvalidRequest exception' do + expect { subject.update_message(message) }.to raise_error(Ably::Exceptions::InvalidRequest, /serial is required/) + end + end + + context 'with a Hash message' do + it 'converts to Message and validates serial' do + expect { subject.update_message({ name: 'test' }) }.to raise_error(Ably::Exceptions::InvalidRequest, /serial is required/) + end + + it 'works when serial is present' do + result = subject.update_message({ name: 'test', serial: serial }) + expect(result).to be_a(Ably::Models::UpdateDeleteResult) + end + end + + context 'does not mutate the original message (#RSL15c)' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + + it 'the original message is unchanged' do + original_json = message.as_json.dup + subject.update_message(message) + expect(message.as_json).to eq(original_json) + expect(message.action).to be_nil + end + end + + context 'with query params (#RSL15f)' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + + it 'passes params as qs_params' do + expect(client).to receive(:patch).with( + anything, + anything, + { qs_params: { 'key' => 'value' } } + ).and_return(patch_response) + + subject.update_message(message, nil, { 'key' => 'value' }) + end + end + end end From 59f16b3e52f23469ff6206f9f39fbdf1a1858a94 Mon Sep 17 00:00:00 2001 From: matt423 Date: Fri, 27 Feb 2026 20:24:50 +0000 Subject: [PATCH 2/8] Add PublishResult for REST and Realtime publish (RSL1n) publish now returns PublishResult containing a serials array that maps 1:1 to the published messages, per spec RSL1n / PBR2a. REST: publish returns PublishResult instead of Boolean. This is backward-compatible since PublishResult is truthy. Realtime: publish callbacks now receive PublishResult instead of the Message object. This is a breaking change for callers that access message properties (name, data, etc.) in publish callbacks. This aligns with the spec and matches the JS SDK behavior. The ACK handler extracts serials from the protocol v5 res field (TR4s) and constructs per-message PublishResults, which are aggregated for multi-message publishes in the Publisher module. --- lib/ably/models/publish_result.rb | 32 +++++++++ lib/ably/realtime/channel/publisher.rb | 11 ++- .../client/incoming_message_dispatcher.rb | 24 ++++++- lib/ably/rest/channel.rb | 14 +++- spec/acceptance/realtime/auth_spec.rb | 4 +- spec/acceptance/realtime/message_spec.rb | 3 +- spec/acceptance/rest/channel_spec.rb | 26 +++---- spec/acceptance/rest/client_spec.rb | 6 +- spec/acceptance/rest/encoders_spec.rb | 2 +- spec/acceptance/rest/message_spec.rb | 2 +- spec/unit/models/publish_result_spec.rb | 68 +++++++++++++++++++ .../incoming_message_dispatcher_spec.rb | 56 +++++++++++++++ spec/unit/rest/channel_spec.rb | 49 ++++++++++--- 13 files changed, 260 insertions(+), 37 deletions(-) create mode 100644 lib/ably/models/publish_result.rb create mode 100644 spec/unit/models/publish_result_spec.rb diff --git a/lib/ably/models/publish_result.rb b/lib/ably/models/publish_result.rb new file mode 100644 index 000000000..897f16efc --- /dev/null +++ b/lib/ably/models/publish_result.rb @@ -0,0 +1,32 @@ +module Ably::Models + # Contains the result of a publish operation. + # + # @spec RSL1n + # + class PublishResult + include Ably::Modules::ModelCommon + + # @param attributes [Hash] + # @option attributes [Array] :serials An array of nullable strings corresponding 1:1 + # to the published messages. Each serial identifies the message on the channel. + # + def initialize(attributes = {}) + @hash_object = IdiomaticRubyWrapper(attributes || {}, stop_at: [:serials]) + @hash_object.freeze + end + + # An array of serial strings (or nil entries), corresponding 1:1 to the published messages. + # + # @spec RSL1n + # + # @return [Array] + # + def serials + attributes[:serials] || [] + end + + def attributes + @hash_object + end + end +end diff --git a/lib/ably/realtime/channel/publisher.rb b/lib/ably/realtime/channel/publisher.rb index 83b3dd85e..ee71836cd 100644 --- a/lib/ably/realtime/channel/publisher.rb +++ b/lib/ably/realtime/channel/publisher.rb @@ -52,13 +52,18 @@ def deferrable_for_multiple_messages(messages) expected_deliveries = messages.count actual_deliveries = 0 failed = false + results = Array.new(messages.count) Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| - messages.each do |message| - message.callback do + messages.each_with_index do |message, index| + message.callback do |result| next if failed + results[index] = result actual_deliveries += 1 - deferrable.succeed messages if actual_deliveries == expected_deliveries + if actual_deliveries == expected_deliveries + all_serials = results.flat_map(&:serials) + deferrable.succeed Ably::Models::PublishResult.new(serials: all_serials) + end end message.errback do |error| next if failed diff --git a/lib/ably/realtime/client/incoming_message_dispatcher.rb b/lib/ably/realtime/client/incoming_message_dispatcher.rb index 2ad53f4b5..1d60d192c 100644 --- a/lib/ably/realtime/client/incoming_message_dispatcher.rb +++ b/lib/ably/realtime/client/incoming_message_dispatcher.rb @@ -199,18 +199,36 @@ def ack_messages(messages, publish_result = nil) logger.debug { "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}" } if publish_result && message.respond_to?(:action) && message.action && message.action.match_any?(Ably::Models::Message::ACTION.MessageUpdate) - serials = publish_result.is_a?(Hash) ? - (publish_result['serials'] || publish_result[:serials]) : - publish_result[:serials] + serials = extract_serials(publish_result) version_serial = serials[index] if serials result = Ably::Models::UpdateDeleteResult.new(version_serial: version_serial) message.succeed result + elsif publish_result && message.is_a?(Ably::Models::Message) + serials = extract_serials(publish_result) + serial = serials ? serials[index] : nil + result = Ably::Models::PublishResult.new(serials: [serial]) + message.succeed result else message.succeed message end end end + def extract_serials(publish_result) + return nil unless publish_result + + if publish_result.is_a?(Hash) + publish_result['serials'] || publish_result[:serials] + elsif publish_result.respond_to?(:[]) + publish_result[:serials] + else + nil + end + rescue StandardError => e + logger.warn { "Failed to extract serials from publish_result (#{publish_result.class}): #{e.message}" } + nil + end + def nack_messages(messages, protocol_message) messages.each do |message| logger.debug { "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}" } diff --git a/lib/ably/rest/channel.rb b/lib/ably/rest/channel.rb index 586ebedca..fc9591c56 100644 --- a/lib/ably/rest/channel.rb +++ b/lib/ably/rest/channel.rb @@ -46,7 +46,7 @@ def initialize(client, name, channel_options = {}) # @param name [String, Array, Ably::Models::Message, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs, or a single Ably::Model::Message object # @param data [String, Array, Hash, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument, in which case an optional hash of query parameters # @param attributes [Hash, nil] Optional additional message attributes such as :extras, :id, :client_id or :connection_id, applied when name attribute is nil or a string (Deprecated, will be removed in 2.0 in favour of constructing a Message object) - # @return [Boolean] true if the message was published, otherwise false + # @return [Ably::Models::PublishResult] A {Ably::Models::PublishResult} containing the serials of the published messages. # # @example # # Publish a single message with (name, data) form @@ -112,7 +112,7 @@ def publish(name, data = nil, attributes = {}) options = qs_params ? { qs_params: qs_params } : {} response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload, options) - [201, 204].include?(response.status) + parse_publish_result(response) end # Updates a previously published message on the channel. Uses patch semantics: non-null fields @@ -224,6 +224,16 @@ def status private + def parse_publish_result(response) + body = response.body + if body.is_a?(Hash) + serials = body['serials'] || body[:serials] || [] + Ably::Models::PublishResult.new(serials: serials) + else + Ably::Models::PublishResult.new(serials: []) + end + end + def base_path "/channels/#{URI.encode_www_form_component(name)}" end diff --git a/spec/acceptance/realtime/auth_spec.rb b/spec/acceptance/realtime/auth_spec.rb index 435070bff..a8fae33e5 100644 --- a/spec/acceptance/realtime/auth_spec.rb +++ b/spec/acceptance/realtime/auth_spec.rb @@ -1243,8 +1243,8 @@ def disconnect_transport(connection) forbidden_channel.publish('not-allowed').errback do |error| expect(error.code).to eql(40160) - allowed_channel.publish(message_name) do |message| - expect(message.name).to eql(message_name) + allowed_channel.publish(message_name) do |result| + expect(result).to be_a(Ably::Models::PublishResult) stop_reactor end end diff --git a/spec/acceptance/realtime/message_spec.rb b/spec/acceptance/realtime/message_spec.rb index b218a8e13..68d3f6094 100644 --- a/spec/acceptance/realtime/message_spec.rb +++ b/spec/acceptance/realtime/message_spec.rb @@ -25,10 +25,11 @@ it 'sends a String data payload' do channel.attach channel.on(:attached) do - channel.publish('test_event', payload) do |message| + channel.subscribe do |message| expect(message.data).to eql(payload) stop_reactor end + channel.publish('test_event', payload) end end diff --git a/spec/acceptance/rest/channel_spec.rb b/spec/acceptance/rest/channel_spec.rb index 37a4e1748..d58145ba3 100644 --- a/spec/acceptance/rest/channel_spec.rb +++ b/spec/acceptance/rest/channel_spec.rb @@ -20,8 +20,8 @@ let(:data) { 'woop!' } context 'with name and data arguments' do - it 'publishes the message and return true indicating success' do - expect(channel.publish(name, data)).to eql(true) + it 'publishes the message and returns a PublishResult' do + expect(channel.publish(name, data)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.first.name).to eql(name) expect(channel.history.items.first.data).to eql(data) end @@ -29,8 +29,8 @@ context 'and additional attributes' do let(:client_id) { random_str } - it 'publishes the message with the attributes and return true indicating success' do - expect(channel.publish(name, data, client_id: client_id)).to eql(true) + it 'publishes the message with the attributes and returns a PublishResult' do + expect(channel.publish(name, data, client_id: client_id)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.first.client_id).to eql(client_id) end end @@ -43,9 +43,9 @@ it 'publishes the message without a client_id' do expect(client).to receive(:post). with("/channels/#{channel_name}/publish", hash_excluding(client_id: client_id), {}). - and_return(double('response', status: 201)) + and_return(double('response', status: 201, body: {})) - expect(channel.publish(name, data)).to eql(true) + expect(channel.publish(name, data)).to be_a(Ably::Models::PublishResult) end it 'expects a client_id to be added by the realtime service' do @@ -66,7 +66,7 @@ expect(messages.sum(&:size) < Ably::Rest::Client::MAX_MESSAGE_SIZE).to eq(true) expect(client).to receive(:post).once.and_call_original - expect(channel.publish(messages)).to eql(true) + expect(channel.publish(messages)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.map(&:name)).to match_array(messages.map { |message| message[:name] }) expect(channel.history.items.map(&:data)).to match_array(messages.map { |message| message[:data] }) end @@ -89,7 +89,7 @@ it 'publishes an array of messages in one HTTP request' do expect(messages.sum &:size).to eq(130) expect(client).to receive(:post).once.and_call_original - expect(channel.publish(messages)).to eql(true) + expect(channel.publish(messages)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.map(&:name)).to match_array(messages.map(&:name)) expect(channel.history.items.map(&:data)).to match_array(messages.map(&:data)) end @@ -127,7 +127,7 @@ it 'publishes an array of messages in one HTTP request' do expect(messages.sum &:size).to eq(130) expect(client).to receive(:post).once.and_call_original - expect(channel.publish(messages)).to eql(true) + expect(channel.publish(messages)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.map(&:name)).to match_array(messages.map(&:name)) expect(channel.history.items.map(&:data)).to match_array(messages.map(&:data)) end @@ -157,7 +157,7 @@ it 'publishes the message' do expect(client).to receive(:post).once.and_call_original - expect(channel.publish(message)).to eql(true) + expect(channel.publish(message)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.first.name).to eql(name) end end @@ -201,7 +201,7 @@ it 'publishes the message without a name attribute in the payload' do expect(client).to receive(:post).with(anything, { "data" => data }, {}).once.and_call_original - expect(channel.publish(nil, data)).to eql(true) + expect(channel.publish(nil, data)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.first.name).to be_nil expect(channel.history.items.first.data).to eql(data) end @@ -212,7 +212,7 @@ it 'publishes the message without a data attribute in the payload' do expect(client).to receive(:post).with(anything, { "name" => name }, {}).once.and_call_original - expect(channel.publish(name)).to eql(true) + expect(channel.publish(name)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.first.name).to eql(name) expect(channel.history.items.first.data).to be_nil end @@ -223,7 +223,7 @@ it 'publishes the message without any attributes in the payload' do expect(client).to receive(:post).with(anything, {}, {}).once.and_call_original - expect(channel.publish(nil)).to eql(true) + expect(channel.publish(nil)).to be_a(Ably::Models::PublishResult) expect(channel.history.items.first.name).to be_nil expect(channel.history.items.first.data).to be_nil end diff --git a/spec/acceptance/rest/client_spec.rb b/spec/acceptance/rest/client_spec.rb index 219ba75bb..fab41c422 100644 --- a/spec/acceptance/rest/client_spec.rb +++ b/spec/acceptance/rest/client_spec.rb @@ -1096,14 +1096,14 @@ def encode64(text) it 'sends a protocol version and lib version header (#G4, #RSC7a, #RSC7b)' do response = client.channels.get('foo').publish("event") - expect(response).to eql true + expect(response).to be_a(Ably::Models::PublishResult) expect(publish_message_stub).to have_been_requested if agent.nil? expect(publish_message_stub.to_s).to include("'Ably-Agent'=>'#{Ably::AGENT}'") - expect(publish_message_stub.to_s).to include("'X-Ably-Version'=>'2'") + expect(publish_message_stub.to_s).to include("'X-Ably-Version'=>'#{Ably::PROTOCOL_VERSION}'") else expect(publish_message_stub.to_s).to include("'Ably-Agent'=>'ably-ruby/1.1.1 ruby/3.1.1'") - expect(publish_message_stub.to_s).to include("'X-Ably-Version'=>'2'") + expect(publish_message_stub.to_s).to include("'X-Ably-Version'=>'#{Ably::PROTOCOL_VERSION}'") end end end diff --git a/spec/acceptance/rest/encoders_spec.rb b/spec/acceptance/rest/encoders_spec.rb index f09ac7209..509349543 100644 --- a/spec/acceptance/rest/encoders_spec.rb +++ b/spec/acceptance/rest/encoders_spec.rb @@ -7,7 +7,7 @@ let(:client) { Ably::Rest::Client.new(default_client_options.merge(protocol: protocol)) } let(:channel_options) { {} } let(:channel) { client.channel('test', channel_options) } - let(:response) { instance_double('Faraday::Response', status: 201) } + let(:response) { instance_double('Faraday::Response', status: 201, body: {}) } let(:cipher_params) { { key: Ably::Util::Crypto.generate_random_key(128), algorithm: 'aes', mode: 'cbc', key_length: 128 } } let(:crypto) { Ably::Util::Crypto.new(cipher_params) } diff --git a/spec/acceptance/rest/message_spec.rb b/spec/acceptance/rest/message_spec.rb index 49960226e..672f4efed 100644 --- a/spec/acceptance/rest/message_spec.rb +++ b/spec/acceptance/rest/message_spec.rb @@ -381,7 +381,7 @@ def mock_for_two_publish_failures expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, '')) expect(message['data']).to eql(encrypted_data_decoded) end - end.and_return(double('Response', status: 201)) + end.and_return(double('Response', status: 201, body: {})) encrypted_channel.publish 'example', encoded_data_decoded end diff --git a/spec/unit/models/publish_result_spec.rb b/spec/unit/models/publish_result_spec.rb new file mode 100644 index 000000000..5a724c837 --- /dev/null +++ b/spec/unit/models/publish_result_spec.rb @@ -0,0 +1,68 @@ +# encoding: utf-8 +require 'spec_helper' + +describe Ably::Models::PublishResult do + subject { Ably::Models::PublishResult } + + context '#serials (#RSL1n)' do + context 'when present' do + let(:model) { subject.new(serials: ['serial-001', 'serial-002']) } + + it 'returns the serials array' do + expect(model.serials).to eql(['serial-001', 'serial-002']) + end + end + + context 'with nullable entries' do + let(:model) { subject.new(serials: ['serial-001', nil, 'serial-003']) } + + it 'preserves nil entries' do + expect(model.serials).to eql(['serial-001', nil, 'serial-003']) + end + end + + context 'when empty array' do + let(:model) { subject.new(serials: []) } + + it 'returns empty array' do + expect(model.serials).to eql([]) + end + end + + context 'when nil' do + let(:model) { subject.new(serials: nil) } + + it 'returns empty array' do + expect(model.serials).to eql([]) + end + end + + context 'when not provided' do + let(:model) { subject.new({}) } + + it 'returns empty array' do + expect(model.serials).to eql([]) + end + end + end + + context '#attributes' do + let(:model) { subject.new(serials: ['s1']) } + + it 'returns the underlying attributes' do + expect(model.attributes).to be_a(Ably::Models::IdiomaticRubyWrapper) + end + + it 'prevents modification' do + expect { model.attributes[:serials] = ['changed'] }.to raise_error(/can't modify frozen|FrozenError/) + end + end + + context 'truthiness' do + let(:model) { subject.new(serials: []) } + + it 'is truthy for backward compatibility with boolean publish returns' do + expect(model).to be_truthy + end + end +end diff --git a/spec/unit/realtime/incoming_message_dispatcher_spec.rb b/spec/unit/realtime/incoming_message_dispatcher_spec.rb index 408109ce8..d873a29b5 100644 --- a/spec/unit/realtime/incoming_message_dispatcher_spec.rb +++ b/spec/unit/realtime/incoming_message_dispatcher_spec.rb @@ -162,6 +162,62 @@ end end + context 'with a regular message and publish_result (PublishResult)' do + let(:message) do + Ably::Models::Message.new(name: 'test', data: 'hello') + end + let(:publish_result) { { 'serials' => ['pub-serial-001'] } } + + it 'succeeds with a PublishResult' do + succeeded = false + message.callback do |result| + expect(result).to be_a(Ably::Models::PublishResult) + expect(result.serials).to eql(['pub-serial-001']) + succeeded = true + end + + dispatcher.send(:ack_messages, [message], publish_result) + expect(succeeded).to be(true) + end + end + + context 'with multiple regular messages and publish_result' do + let(:message1) { Ably::Models::Message.new(name: 'test1', data: 'hello') } + let(:message2) { Ably::Models::Message.new(name: 'test2', data: 'world') } + let(:publish_result) { { 'serials' => ['ser-aaa', 'ser-bbb'] } } + + it 'assigns the correct serial to each message by index' do + results = [] + message1.callback { |r| results << r } + message2.callback { |r| results << r } + + dispatcher.send(:ack_messages, [message1, message2], publish_result) + + expect(results.length).to eql(2) + expect(results[0]).to be_a(Ably::Models::PublishResult) + expect(results[0].serials).to eql(['ser-aaa']) + expect(results[1]).to be_a(Ably::Models::PublishResult) + expect(results[1].serials).to eql(['ser-bbb']) + end + end + + context 'with a regular message and symbol-keyed publish_result' do + let(:message) { Ably::Models::Message.new(name: 'test', data: 'hello') } + let(:publish_result) { { serials: ['sym-serial'] } } + + it 'handles symbol keys for serials' do + succeeded = false + message.callback do |result| + expect(result).to be_a(Ably::Models::PublishResult) + expect(result.serials).to eql(['sym-serial']) + succeeded = true + end + + dispatcher.send(:ack_messages, [message], publish_result) + expect(succeeded).to be(true) + end + end + context 'with presence messages' do let(:presence_message) do Ably::Models::PresenceMessage.new(action: :enter, client_id: 'user1') diff --git a/spec/unit/rest/channel_spec.rb b/spec/unit/rest/channel_spec.rb index 3cd8a6893..750b4f693 100644 --- a/spec/unit/rest/channel_spec.rb +++ b/spec/unit/rest/channel_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' describe Ably::Rest::Channel do + let(:post_response) { instance_double('Faraday::Response', status: 201, body: { 'serials' => ['serial-001'] }) } let(:client) do instance_double( 'Ably::Rest::Client', encoders: [], - post: instance_double('Faraday::Response', status: 201), + post: post_response, idempotent_rest_publishing: false, max_message_size: max_message_size ) end @@ -91,7 +92,7 @@ let(:encoding) { Encoding::UTF_8 } it 'is permitted' do - expect(subject.publish(encoded_value, 'data')).to eql(true) + expect(subject.publish(encoded_value, 'data')).to be_a(Ably::Models::PublishResult) end end @@ -100,7 +101,7 @@ let(:encoding) { Encoding::UTF_8 } it 'is permitted' do - expect(subject.publish(encoded_value, 'data')).to eql(true) + expect(subject.publish(encoded_value, 'data')).to be_a(Ably::Models::PublishResult) end end @@ -108,7 +109,7 @@ let(:encoding) { Encoding::SHIFT_JIS } it 'is permitted' do - expect(subject.publish(encoded_value, 'data')).to eql(true) + expect(subject.publish(encoded_value, 'data')).to be_a(Ably::Models::PublishResult) end end @@ -116,7 +117,7 @@ let(:encoding) { Encoding::ASCII_8BIT } it 'is permitted' do - expect(subject.publish(encoded_value, 'data')).to eql(true) + expect(subject.publish(encoded_value, 'data')).to be_a(Ably::Models::PublishResult) end end @@ -148,7 +149,7 @@ context 'and a message size is 10 bytes' do it 'should send a message' do - expect(subject.publish('x' * 10, 'data')).to eq(true) + expect(subject.publish('x' * 10, 'data')).to be_a(Ably::Models::PublishResult) end end end @@ -164,13 +165,45 @@ context 'and a message size is 2 bytes' do it 'should send a message' do - expect(subject.publish('x' * 2, 'data')).to eq(true) + expect(subject.publish('x' * 2, 'data')).to be_a(Ably::Models::PublishResult) end end end end end + describe '#publish returns PublishResult (#RSL1n)' do + context 'with serials in response body' do + let(:post_response) { instance_double('Faraday::Response', status: 201, body: { 'serials' => ['serial-abc', 'serial-def'] }) } + + it 'returns a PublishResult with serials' do + result = subject.publish('event', 'data') + expect(result).to be_a(Ably::Models::PublishResult) + expect(result.serials).to eql(['serial-abc', 'serial-def']) + end + end + + context 'with empty response body (204)' do + let(:post_response) { instance_double('Faraday::Response', status: 204, body: nil) } + + it 'returns a PublishResult with empty serials' do + result = subject.publish('event', 'data') + expect(result).to be_a(Ably::Models::PublishResult) + expect(result.serials).to eql([]) + end + end + + context 'with non-hash response body' do + let(:post_response) { instance_double('Faraday::Response', status: 201, body: '') } + + it 'returns a PublishResult with empty serials' do + result = subject.publish('event', 'data') + expect(result).to be_a(Ably::Models::PublishResult) + expect(result.serials).to eql([]) + end + end + end + describe '#update_message (#RSL15)' do let(:serial) { 'msg-serial-001' } let(:patch_response) { instance_double('Faraday::Response', status: 200, body: { 'versionSerial' => 'v1-serial' }) } @@ -178,7 +211,7 @@ instance_double( 'Ably::Rest::Client', encoders: [], - post: instance_double('Faraday::Response', status: 201), + post: instance_double('Faraday::Response', status: 201, body: { 'serials' => ['serial-001'] }), patch: patch_response, idempotent_rest_publishing: false, max_message_size: max_message_size From 50840f828a7706f02d0ab2f63688020204e0b0e0 Mon Sep 17 00:00:00 2001 From: matt423 Date: Fri, 27 Feb 2026 20:51:50 +0000 Subject: [PATCH 3/8] Add delete_message for REST and Realtime channels (RSL15/RTL32) Extract shared send_message_action helper from update_message and add delete_message using MESSAGE_DELETE action. Extend ACTION enum with all TM5 values (message_delete, meta, message_summary, message_append). Update ACK handler to return UpdateDeleteResult for delete/append actions. --- lib/ably/models/message.rb | 4 + lib/ably/realtime/channel.rb | 120 ++++++++++-------- .../client/incoming_message_dispatcher.rb | 6 +- lib/ably/rest/channel.rb | 78 +++++++----- spec/unit/models/message_spec.rb | 16 +++ spec/unit/rest/channel_spec.rb | 68 ++++++++++ 6 files changed, 209 insertions(+), 83 deletions(-) diff --git a/lib/ably/models/message.rb b/lib/ably/models/message.rb index 02350b71a..4c23c8870 100644 --- a/lib/ably/models/message.rb +++ b/lib/ably/models/message.rb @@ -35,6 +35,10 @@ class Message ACTION = ruby_enum('ACTION', :message_create, # 0 :message_update, # 1 + :message_delete, # 2 + :meta, # 3 + :message_summary, # 4 + :message_append, # 5 ) # Statically register a default set of encoders for this class diff --git a/lib/ably/realtime/channel.rb b/lib/ably/realtime/channel.rb index 65d359fbe..0fa379a99 100644 --- a/lib/ably/realtime/channel.rb +++ b/lib/ably/realtime/channel.rb @@ -227,58 +227,23 @@ def publish(name, data = nil, attributes = {}, &success_block) # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks # def update_message(message, operation = nil, params = {}, &success_block) - if suspended? || failed? - error = Ably::Exceptions::ChannelInactive.new("Cannot update messages on a channel in state #{state}") - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - if !connection.can_publish_messages? - error = Ably::Exceptions::MessageQueueingDisabled.new( - "Message cannot be updated. Client is not allowed to queue messages when connection is in state #{connection.state}" - ) - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - message = Ably::Models::Message(message) - - unless message.serial - error = Ably::Exceptions::InvalidRequest.new('Message serial is required for update operations. Ensure the message has a serial field.') - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - # RTL32c - Do not mutate the user-supplied message; build a new one - update_attrs = message.as_json - update_attrs['action'] = Ably::Models::Message::ACTION.MessageUpdate.to_i - - if operation - op_hash = operation.respond_to?(:as_json) ? operation.as_json : operation - update_attrs['version'] = op_hash - end - - updated_message = Ably::Models::Message.new(update_attrs) - updated_message.encode(client.encoders, options) do |encode_error, error_message| - client.logger.error error_message - end - - pm_params = { action: Ably::Models::ProtocolMessage::ACTION.Message.to_i, channel: name, messages: [updated_message] } - pm_params[:params] = params.transform_values(&:to_s) if params && !params.empty? - - connection.send_protocol_message(pm_params) + send_message_action(message, Ably::Models::Message::ACTION.MessageUpdate, operation, params, &success_block) + end - # Wrap the inner message deferrable to return UpdateDeleteResult - Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| - updated_message.callback do |result| - if result.is_a?(Ably::Models::UpdateDeleteResult) - deferrable.succeed result - else - deferrable.succeed Ably::Models::UpdateDeleteResult.new(version_serial: nil) - end - end - updated_message.errback do |error| - deferrable.fail error - end - deferrable.callback(&success_block) if block_given? - end + # Deletes a previously published message on the channel. A callback may optionally be passed in to this + # call to be notified of success or failure of the operation. + # + # @spec RTL32 + # + # @param message [Ably::Models::Message, Hash] A Message object or Hash containing a populated :serial field. + # @param operation [Hash, Ably::Models::MessageOperation, nil] Optional operation metadata. + # @param params [Hash, nil] Optional parameters sent as part of the protocol message. + # + # @yield [Ably::Models::UpdateDeleteResult] On success, calls the block with the result containing version_serial. + # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks + # + def delete_message(message, operation = nil, params = {}, &success_block) + send_message_action(message, Ably::Models::Message::ACTION.MessageDelete, operation, params, &success_block) end # Registers a listener for messages on this channel. The caller supplies a listener function, which is called @@ -473,6 +438,59 @@ def need_reattach? private + def send_message_action(message, action_enum, operation = nil, params = {}, &success_block) + if suspended? || failed? + error = Ably::Exceptions::ChannelInactive.new("Cannot send message action on a channel in state #{state}") + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + if !connection.can_publish_messages? + error = Ably::Exceptions::MessageQueueingDisabled.new( + "Message action cannot be sent. Client is not allowed to queue messages when connection is in state #{connection.state}" + ) + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + message = Ably::Models::Message(message) + + unless message.serial + error = Ably::Exceptions::InvalidRequest.new('Message serial is required. Ensure the message has a serial field.') + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + update_attrs = message.as_json + update_attrs['action'] = action_enum.to_i + + if operation + op_hash = operation.respond_to?(:as_json) ? operation.as_json : operation + update_attrs['version'] = op_hash + end + + updated_message = Ably::Models::Message.new(update_attrs) + updated_message.encode(client.encoders, options) do |encode_error, error_message| + client.logger.error error_message + end + + pm_params = { action: Ably::Models::ProtocolMessage::ACTION.Message.to_i, channel: name, messages: [updated_message] } + pm_params[:params] = params.transform_values(&:to_s) if params && !params.empty? + + connection.send_protocol_message(pm_params) + + Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| + updated_message.callback do |result| + if result.is_a?(Ably::Models::UpdateDeleteResult) + deferrable.succeed result + else + deferrable.succeed Ably::Models::UpdateDeleteResult.new(version_serial: nil) + end + end + updated_message.errback do |error| + deferrable.fail error + end + deferrable.callback(&success_block) if block_given? + end + end + def setup_event_handlers __incoming_msgbus__.subscribe(:message) do |message| message.decode(client.encoders, options) do |encode_error, error_message| diff --git a/lib/ably/realtime/client/incoming_message_dispatcher.rb b/lib/ably/realtime/client/incoming_message_dispatcher.rb index 1d60d192c..c19a5ff81 100644 --- a/lib/ably/realtime/client/incoming_message_dispatcher.rb +++ b/lib/ably/realtime/client/incoming_message_dispatcher.rb @@ -198,7 +198,11 @@ def ack_messages(messages, publish_result = nil) messages.each_with_index do |message, index| logger.debug { "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}" } if publish_result && message.respond_to?(:action) && message.action && - message.action.match_any?(Ably::Models::Message::ACTION.MessageUpdate) + message.action.match_any?( + Ably::Models::Message::ACTION.MessageUpdate, + Ably::Models::Message::ACTION.MessageDelete, + Ably::Models::Message::ACTION.MessageAppend + ) serials = extract_serials(publish_result) version_serial = serials[index] if serials result = Ably::Models::UpdateDeleteResult.new(version_serial: version_serial) diff --git a/lib/ably/rest/channel.rb b/lib/ably/rest/channel.rb index fc9591c56..54b016d55 100644 --- a/lib/ably/rest/channel.rb +++ b/lib/ably/rest/channel.rb @@ -115,9 +115,7 @@ def publish(name, data = nil, attributes = {}) parse_publish_result(response) end - # Updates a previously published message on the channel. Uses patch semantics: non-null fields - # in the provided message will replace the corresponding fields in the existing message, while - # null fields will be left unchanged. + # Updates a previously published message on the channel. # # @spec RSL15 # @@ -130,35 +128,22 @@ def publish(name, data = nil, attributes = {}) # @return [Ably::Models::UpdateDeleteResult] The result containing the version_serial. # def update_message(message, operation = nil, params = {}) - message = Ably::Models::Message(message) - - raise Ably::Exceptions::InvalidRequest.new( - 'Message serial is required for update operations. Ensure the message has a serial field.' - ) unless message.serial - - # RSL15c - Do not mutate the user-supplied message; build a new one - update_attrs = message.as_json - update_attrs['action'] = Ably::Models::Message::ACTION.MessageUpdate.to_i - - if operation - op_hash = operation.respond_to?(:as_json) ? operation.as_json : operation - update_attrs['version'] = op_hash - end - - updated_message = Ably::Models::Message.new(update_attrs) - updated_message.encode client.encoders, options - - payload = updated_message.as_json - serial = message.serial - - request_options = params && !params.empty? ? { qs_params: params } : {} - response = client.patch( - "#{base_path}/messages/#{URI.encode_www_form_component(serial)}", - payload, - request_options - ) + send_message_action(message, Ably::Models::Message::ACTION.MessageUpdate, operation, params) + end - Ably::Models::UpdateDeleteResult.new(response.body || {}) + # Deletes a previously published message on the channel. + # + # @spec RSL15 + # + # @param message [Ably::Models::Message, Hash] A Message object or Hash containing a populated :serial field. + # @param operation [Hash, Ably::Models::MessageOperation, nil] Optional operation metadata containing + # :description and/or :metadata fields. + # @param params [Hash, nil] Optional parameters sent as part of the query string. + # + # @return [Ably::Models::UpdateDeleteResult] The result containing the version_serial. + # + def delete_message(message, operation = nil, params = {}) + send_message_action(message, Ably::Models::Message::ACTION.MessageDelete, operation, params) end # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical {Ably::Models::Message} @@ -224,6 +209,37 @@ def status private + def send_message_action(message, action_enum, operation = nil, params = {}) + message = Ably::Models::Message(message) + + raise Ably::Exceptions::InvalidRequest.new( + 'Message serial is required. Ensure the message has a serial field.' + ) unless message.serial + + update_attrs = message.as_json + update_attrs['action'] = action_enum.to_i + + if operation + op_hash = operation.respond_to?(:as_json) ? operation.as_json : operation + update_attrs['version'] = op_hash + end + + updated_message = Ably::Models::Message.new(update_attrs) + updated_message.encode client.encoders, options + + payload = updated_message.as_json + serial = message.serial + + request_options = params && !params.empty? ? { qs_params: params } : {} + response = client.patch( + "#{base_path}/messages/#{URI.encode_www_form_component(serial)}", + payload, + request_options + ) + + Ably::Models::UpdateDeleteResult.new(response.body || {}) + end + def parse_publish_result(response) body = response.body if body.is_a?(Hash) diff --git a/spec/unit/models/message_spec.rb b/spec/unit/models/message_spec.rb index c5e0f1390..eb21f78f6 100644 --- a/spec/unit/models/message_spec.rb +++ b/spec/unit/models/message_spec.rb @@ -678,6 +678,22 @@ it 'has message_update as 1' do expect(Ably::Models::Message::ACTION.MessageUpdate.to_i).to eq(1) end + + it 'has message_delete as 2' do + expect(Ably::Models::Message::ACTION.MessageDelete.to_i).to eq(2) + end + + it 'has meta as 3' do + expect(Ably::Models::Message::ACTION.Meta.to_i).to eq(3) + end + + it 'has message_summary as 4' do + expect(Ably::Models::Message::ACTION.MessageSummary.to_i).to eq(4) + end + + it 'has message_append as 5' do + expect(Ably::Models::Message::ACTION.MessageAppend.to_i).to eq(5) + end end end diff --git a/spec/unit/rest/channel_spec.rb b/spec/unit/rest/channel_spec.rb index 750b4f693..8a1c8277e 100644 --- a/spec/unit/rest/channel_spec.rb +++ b/spec/unit/rest/channel_spec.rb @@ -320,4 +320,72 @@ end end end + + describe '#delete_message (#RSL15)' do + let(:serial) { 'msg-serial-001' } + let(:patch_response) { instance_double('Faraday::Response', status: 200, body: { 'versionSerial' => 'v1-serial' }) } + let(:client) do + instance_double( + 'Ably::Rest::Client', + encoders: [], + post: instance_double('Faraday::Response', status: 201, body: { 'serials' => ['serial-001'] }), + patch: patch_response, + idempotent_rest_publishing: false, + max_message_size: max_message_size + ) + end + + context 'with a valid message containing serial' do + let(:message) { Ably::Models::Message.new(name: 'test', data: 'hello', serial: serial) } + + it 'sends a PATCH request with action MESSAGE_DELETE' do + expect(client).to receive(:patch).with( + "/channels/#{channel_name}/messages/#{serial}", + hash_including('action' => Ably::Models::Message::ACTION.MessageDelete.to_i), + {} + ).and_return(patch_response) + + subject.delete_message(message) + end + + it 'returns an UpdateDeleteResult' do + result = subject.delete_message(message) + expect(result).to be_a(Ably::Models::UpdateDeleteResult) + expect(result.version_serial).to eql('v1-serial') + end + end + + context 'with an operation parameter' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + let(:operation) { { description: 'Removed by moderator' } } + + it 'includes the operation as version in the payload' do + expect(client).to receive(:patch) do |_path, payload, _opts| + expect(payload['version']).to eq({ 'description' => 'Removed by moderator' }) + patch_response + end + + subject.delete_message(message, operation) + end + end + + context 'without serial' do + let(:message) { Ably::Models::Message.new(name: 'test', data: 'hello') } + + it 'raises an InvalidRequest exception' do + expect { subject.delete_message(message) }.to raise_error(Ably::Exceptions::InvalidRequest, /serial is required/) + end + end + + context 'does not mutate the original message' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + + it 'the original message is unchanged' do + original_json = message.as_json.dup + subject.delete_message(message) + expect(message.as_json).to eq(original_json) + expect(message.action).to be_nil + end + end + end end From 30f46190319db8c713b20a4d4b59552a1f7de06c Mon Sep 17 00:00:00 2001 From: matt423 Date: Fri, 27 Feb 2026 20:55:12 +0000 Subject: [PATCH 4/8] Add append_message for REST and Realtime channels (RSL15/RTL32) Add append_message using MESSAGE_APPEND action via the shared send_message_action helper. Same signature and behavior as update_message and delete_message. --- lib/ably/realtime/channel.rb | 17 +++++++++ lib/ably/rest/channel.rb | 16 ++++++++ spec/unit/rest/channel_spec.rb | 68 ++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/lib/ably/realtime/channel.rb b/lib/ably/realtime/channel.rb index 0fa379a99..8273a435d 100644 --- a/lib/ably/realtime/channel.rb +++ b/lib/ably/realtime/channel.rb @@ -246,6 +246,23 @@ def delete_message(message, operation = nil, params = {}, &success_block) send_message_action(message, Ably::Models::Message::ACTION.MessageDelete, operation, params, &success_block) end + # Appends data to a previously published message on the channel. A callback may optionally be passed in to this + # call to be notified of success or failure of the operation. + # + # @spec RTL32 + # + # @param message [Ably::Models::Message, Hash] A Message object or Hash containing a populated :serial field + # and the data to append. + # @param operation [Hash, Ably::Models::MessageOperation, nil] Optional operation metadata. + # @param params [Hash, nil] Optional parameters sent as part of the protocol message. + # + # @yield [Ably::Models::UpdateDeleteResult] On success, calls the block with the result containing version_serial. + # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks + # + def append_message(message, operation = nil, params = {}, &success_block) + send_message_action(message, Ably::Models::Message::ACTION.MessageAppend, operation, params, &success_block) + end + # Registers a listener for messages on this channel. The caller supplies a listener function, which is called # each time one or more messages arrives on the channel. A callback may optionally be passed in to this call # to be notified of success or failure of the channel {Ably::Realtime::Channel#attach} operation. diff --git a/lib/ably/rest/channel.rb b/lib/ably/rest/channel.rb index 54b016d55..19d5827b5 100644 --- a/lib/ably/rest/channel.rb +++ b/lib/ably/rest/channel.rb @@ -146,6 +146,22 @@ def delete_message(message, operation = nil, params = {}) send_message_action(message, Ably::Models::Message::ACTION.MessageDelete, operation, params) end + # Appends data to a previously published message on the channel. + # + # @spec RSL15 + # + # @param message [Ably::Models::Message, Hash] A Message object or Hash containing a populated :serial field + # and the data to append. + # @param operation [Hash, Ably::Models::MessageOperation, nil] Optional operation metadata containing + # :description and/or :metadata fields. + # @param params [Hash, nil] Optional parameters sent as part of the query string. + # + # @return [Ably::Models::UpdateDeleteResult] The result containing the version_serial. + # + def append_message(message, operation = nil, params = {}) + send_message_action(message, Ably::Models::Message::ACTION.MessageAppend, operation, params) + end + # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical {Ably::Models::Message} # objects for the channel. If the channel is configured to persist messages, then messages can be retrieved from # history for up to 72 hours in the past. If not, messages can only be retrieved from history for up to two minutes in the past. diff --git a/spec/unit/rest/channel_spec.rb b/spec/unit/rest/channel_spec.rb index 8a1c8277e..11ef513e3 100644 --- a/spec/unit/rest/channel_spec.rb +++ b/spec/unit/rest/channel_spec.rb @@ -388,4 +388,72 @@ end end end + + describe '#append_message (#RSL15)' do + let(:serial) { 'msg-serial-001' } + let(:patch_response) { instance_double('Faraday::Response', status: 200, body: { 'versionSerial' => 'v1-serial' }) } + let(:client) do + instance_double( + 'Ably::Rest::Client', + encoders: [], + post: instance_double('Faraday::Response', status: 201, body: { 'serials' => ['serial-001'] }), + patch: patch_response, + idempotent_rest_publishing: false, + max_message_size: max_message_size + ) + end + + context 'with a valid message containing serial' do + let(:message) { Ably::Models::Message.new(name: 'test', data: ' appended', serial: serial) } + + it 'sends a PATCH request with action MESSAGE_APPEND' do + expect(client).to receive(:patch).with( + "/channels/#{channel_name}/messages/#{serial}", + hash_including('action' => Ably::Models::Message::ACTION.MessageAppend.to_i), + {} + ).and_return(patch_response) + + subject.append_message(message) + end + + it 'returns an UpdateDeleteResult' do + result = subject.append_message(message) + expect(result).to be_a(Ably::Models::UpdateDeleteResult) + expect(result.version_serial).to eql('v1-serial') + end + end + + context 'with an operation parameter' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + let(:operation) { { description: 'Added suffix' } } + + it 'includes the operation as version in the payload' do + expect(client).to receive(:patch) do |_path, payload, _opts| + expect(payload['version']).to eq({ 'description' => 'Added suffix' }) + patch_response + end + + subject.append_message(message, operation) + end + end + + context 'without serial' do + let(:message) { Ably::Models::Message.new(name: 'test', data: 'hello') } + + it 'raises an InvalidRequest exception' do + expect { subject.append_message(message) }.to raise_error(Ably::Exceptions::InvalidRequest, /serial is required/) + end + end + + context 'does not mutate the original message' do + let(:message) { Ably::Models::Message.new(name: 'test', serial: serial) } + + it 'the original message is unchanged' do + original_json = message.as_json.dup + subject.append_message(message) + expect(message.as_json).to eq(original_json) + expect(message.action).to be_nil + end + end + end end From fc0f640f37ed5cc673b150f1affb82d48f72bacc Mon Sep 17 00:00:00 2001 From: matt423 Date: Mon, 2 Mar 2026 11:50:15 +0000 Subject: [PATCH 5/8] Rewrite Stats model to match spec v2.1+ (TS12) The protocol version change from 2 to 5 changed the stats API response format from nested objects to a flat entries dictionary (TS12r). Update the Stats model to be spec-compliant: - Replace nested accessors (all, inbound, outbound, etc.) with flat entries hash - Add unit, in_progress, schema, app_id accessors (TS12c/q/s/t) - Remove deleted sub-types: MessageTraffic, MessageTypes, MessageCount, ConnectionTypes, RequestCount, ResourceCount (TS4-TS9) - Delete stats_types.rb --- lib/ably/models/stats.rb | 125 ++++++----------- lib/ably/models/stats_types.rb | 107 --------------- spec/acceptance/rest/stats_spec.rb | 72 +++++----- spec/unit/models/stats_spec.rb | 209 +++++------------------------ 4 files changed, 116 insertions(+), 397 deletions(-) delete mode 100644 lib/ably/models/stats_types.rb diff --git a/lib/ably/models/stats.rb b/lib/ably/models/stats.rb index f7d919998..c13c8c27f 100644 --- a/lib/ably/models/stats.rb +++ b/lib/ably/models/stats.rb @@ -1,5 +1,3 @@ -require 'ably/models/stats_types' - module Ably::Models # Convert stat argument to a {Stats} object # @@ -17,16 +15,15 @@ def self.Stats(stat) # A class representing an individual statistic for a specified {#interval_id} # + # @spec TS12 + # class Stats include Ably::Modules::ModelCommon extend Ably::Modules::Enum # Describes the interval unit over which statistics are gathered. # - # MINUTE Interval unit over which statistics are gathered as minutes. - # HOUR Interval unit over which statistics are gathered as hours. - # DAY Interval unit over which statistics are gathered as days. - # MONTH Interval unit over which statistics are gathered as months. + # @spec TS12c # GRANULARITY = ruby_enum('GRANULARITY', :minute, @@ -103,123 +100,83 @@ def expected_length(format) # {Stats} initializer # - # @param hash_object [Hash] object with the underlying stat details + # @param hash_object [Hash] object with the underlying stat details # def initialize(hash_object) - @raw_hash_object = hash_object + @raw_hash_object = hash_object set_attributes_object hash_object end - # A {Ably::Models::Stats::MessageTypes} object containing the aggregate count of all message stats. + # The interval ID for this stats object. # - # @spec TS12e - # - # @return [Stats::MessageTypes] - # - def all - @all ||= Stats::MessageTypes.new(attributes[:all]) - end - - # A {Ably::Models::Stats::MessageTraffic} object containing the aggregate count of inbound message stats. - # - # @spec TS12f - # - # @return [Ably::Models::Stats::MessageTraffic] - # - def inbound - @inbound ||= Stats::MessageTraffic.new(attributes[:inbound]) - end - - # A {Ably::Models::Stats::MessageTraffic} object containing the aggregate count of outbound message stats. - # - # @spec TS12g - # - # @return [Ably::Models::Stats::MessageTraffic] - # - def outbound - @outbound ||= Stats::MessageTraffic.new(attributes[:outbound]) - end - - # A {Ably::Models::Stats::MessageTraffic} object containing the aggregate count of persisted message stats. - # - # @spec TS12h + # @spec TS12a # - # @return [Ably::Models::Stats::MessageTraffic] + # @return [String] # - def persisted - @persisted ||= Stats::MessageTypes.new(attributes[:persisted]) + def interval_id + attributes.fetch(:interval_id) end - # A {Ably::Models::Stats::ConnectionTypes} object containing a breakdown of connection related stats, such as min, mean and peak connections. + # Represents the intervalId as a time object. # - # @spec TS12i + # @spec TS12p # - # @return [Ably::Models::Stats::ConnectionTypes] + # @return [Time] # - def connections - @connections ||= Stats::ConnectionTypes.new(attributes[:connections]) + def interval_time + self.class.from_interval_id(interval_id) end - # A {Ably::Models::Stats::ResourceCount} object containing a breakdown of connection related stats, such as min, mean and peak connections. + # The unit of the interval, as provided by the API response. # - # @spec TS12j + # @spec TS12c # - # @return [Ably::Models::Stats::ResourceCount] + # @return [String] # - def channels - @channels ||= Stats::ResourceCount.new(attributes[:channels]) + def unit + attributes[:unit] end - # A {Ably::Models::Stats::RequestCount} object containing a breakdown of API Requests. + # For entries that are still in progress, the last sub-interval included. # - # @spec TS12k + # @spec TS12q # - # @return [Ably::Models::Stats::RequestCount] + # @return [String, nil] # - def api_requests - @api_requests ||= Stats::RequestCount.new(attributes[:api_requests]) + def in_progress + attributes[:in_progress] end - # A {Ably::Models::Stats::RequestCount} object containing a breakdown of Ably Token requests. + # A flat dictionary of statistics entries with dot-separated keys. # - # @spec TS12l + # @spec TS12r # - # @return [Ably::Models::Stats::RequestCount] + # @return [Hash] # - def token_requests - @token_requests ||= Stats::RequestCount.new(attributes[:token_requests]) + def entries + raw_entries = raw_hash_object[:entries] || raw_hash_object['entries'] + return {} unless raw_entries + raw_entries.is_a?(Hash) ? raw_entries : {} end - # The UTC time at which the time period covered begins. If unit is set to minute this will be in - # the format YYYY-mm-dd:HH:MM, if hour it will be YYYY-mm-dd:HH, if day it will be YYYY-mm-dd:00 - # and if month it will be YYYY-mm-01:00. + # The JSON schema URI for this stats object. # - # @spec TS12a + # @spec TS12s # # @return [String] # - def interval_id - attributes.fetch(:interval_id) + def schema + attributes[:schema] end - # Represents the intervalId as a time object. + # The Ably application ID this stats object relates to. # - # @spec TS12b + # @spec TS12t # - # @return [Time] - # - def interval_time - self.class.from_interval_id(interval_id) - end - - # The length of the interval the stats span. Values will be a [StatsIntervalGranularity]{@link StatsIntervalGranularity}. - # - # @spec TS12c - # - # @return [GRANULARITY] The granularity of the interval for the stat such as :day, :hour, :minute, see {GRANULARITY} + # @return [String] # - def interval_granularity - self.class.granularity_from_interval_id(interval_id) + def app_id + attributes[:app_id] end def attributes diff --git a/lib/ably/models/stats_types.rb b/lib/ably/models/stats_types.rb deleted file mode 100644 index 54eaa8672..000000000 --- a/lib/ably/models/stats_types.rb +++ /dev/null @@ -1,107 +0,0 @@ -module Ably::Models - class Stats - # StatsStruct is a basic Struct like class that allows methods to be defined - # on the class that will be retuned co-erced objects from the underlying hash used to - # initialize the object. - # - # This class provides a concise way to create classes that have fixed attributes and types - # - # @example - # class MessageCount < StatsStruct - # coerce_attributes :count, :data, into: Integer - # end - # - # @api private - # - class StatsStruct - class << self - def coerce_attributes(*attributes) - options = attributes.pop - raise ArgumentError, 'Expected attribute into: within options hash' unless options.kind_of?(Hash) && options[:into] - - @type_klass = options[:into] - setup_attribute_methods attributes - end - - def type_klass - @type_klass - end - - private - def setup_attribute_methods(attributes) - attributes.each do |attr| - define_method(attr) do - # Lazy load the co-erced value only when accessed - unless instance_variable_defined?("@#{attr}") - instance_variable_set "@#{attr}", self.class.type_klass.new(hash[attr.to_sym]) - end - instance_variable_get("@#{attr}") - end - end - end - end - - attr_reader :hash - - def initialize(hash) - @hash = hash || {} - end - end - - # IntegerDefaultZero will always return an Integer object and will default to value 0 unless truthy - # - # @api private - # - class IntegerDefaultZero - def self.new(value) - (value && value.to_i) || 0 - end - end - - # MessageCount contains aggregate counts for messages and data transferred - # - # @spec TS5a, TS5b - # - class MessageCount < StatsStruct - coerce_attributes :count, :data, into: IntegerDefaultZero - end - - # RequestCount contains aggregate counts for requests made - # - # @spec TS8a, TS8b, TS8c - # - class RequestCount < StatsStruct - coerce_attributes :succeeded, :failed, :refused, into: IntegerDefaultZero - end - - # ResourceCount contains aggregate data for usage of a resource in a specific scope - # - class ResourceCount < StatsStruct - coerce_attributes :opened, :peak, :mean, :min, :refused, into: IntegerDefaultZero - end - - # ConnectionTypes contains a breakdown of summary stats data for different (TLS vs non-TLS) connection types - # - # @spec TS4a, TS4b, TS4c - # - class ConnectionTypes < StatsStruct - coerce_attributes :tls, :plain, :all, into: ResourceCount - end - - # MessageTypes contains a breakdown of summary stats data for different (message vs presence) message types - # - # @spec TS6a, TS6b, TS6c - # - class MessageTypes < StatsStruct - coerce_attributes :messages, :presence, :all, into: MessageCount - end - - # MessageTraffic contains a breakdown of summary stats data for traffic over various transport types - # - # @spec TS7a, TS7b, TS7c, TS7d - # - class MessageTraffic < StatsStruct - coerce_attributes :realtime, :rest, :webhook, :all, into: MessageTypes - end - end -end diff --git a/spec/acceptance/rest/stats_spec.rb b/spec/acceptance/rest/stats_spec.rb index 1a4d81a03..f3833074a 100644 --- a/spec/acceptance/rest/stats_spec.rb +++ b/spec/acceptance/rest/stats_spec.rb @@ -63,8 +63,8 @@ let(:subject) { client.stats(end: LAST_INTERVAL) } # end is needed to ensure no other tests have effected the stats let(:stat) { subject.items.first } - it 'uses the minute interval by default' do - expect(stat.interval_granularity).to eq(:minute) + it 'returns the unit from the JSON response' do + expect(stat.unit).to eq('minute') end end @@ -76,58 +76,62 @@ expect(subject.items.count).to eql(1) end - it 'returns zero value for any missing metrics' do - expect(stat.channels.refused).to eql(0) - expect(stat.outbound.webhook.all.count).to eql(0) + it 'returns entries as a flat hash (#TS12r)' do + expect(stat.entries).to be_a(Hash) + end + + it 'returns zero or nil for any missing entries' do + expect(stat.entries['channels.refused']).to be_nil + expect(stat.entries['messages.outbound.webhook.all.count']).to be_nil end it 'returns all aggregated message data' do - expect(stat.all.messages.count).to eql(70 + 40) # inbound + outbound - expect(stat.all.messages.data).to eql(7000 + 4000) # inbound + outbound + expect(stat.entries['messages.all.messages.count']).to eql(70 + 40) # inbound + outbound + expect(stat.entries['messages.all.messages.data']).to eql(7000 + 4000) # inbound + outbound end it 'returns inbound realtime all data' do - expect(stat.inbound.realtime.all.count).to eql(70) - expect(stat.inbound.realtime.all.data).to eql(7000) + expect(stat.entries['messages.inbound.realtime.all.count']).to eql(70) + expect(stat.entries['messages.inbound.realtime.all.data']).to eql(7000) end it 'returns inbound realtime message data' do - expect(stat.inbound.realtime.messages.count).to eql(70) - expect(stat.inbound.realtime.messages.data).to eql(7000) + expect(stat.entries['messages.inbound.realtime.messages.count']).to eql(70) + expect(stat.entries['messages.inbound.realtime.messages.data']).to eql(7000) end it 'returns outbound realtime all data' do - expect(stat.outbound.realtime.all.count).to eql(40) - expect(stat.outbound.realtime.all.data).to eql(4000) + expect(stat.entries['messages.outbound.realtime.all.count']).to eql(40) + expect(stat.entries['messages.outbound.realtime.all.data']).to eql(4000) end it 'returns persisted presence all data' do - expect(stat.persisted.all.count).to eql(20) - expect(stat.persisted.all.data).to eql(2000) + expect(stat.entries['messages.persisted.all.count']).to eql(20) + expect(stat.entries['messages.persisted.all.data']).to eql(2000) end it 'returns connections all data' do - expect(stat.connections.tls.peak).to eql(20) - expect(stat.connections.tls.opened).to eql(10) + expect(stat.entries['connections.all.peak']).to eql(20) + expect(stat.entries['connections.all.opened']).to eql(10) end - it 'returns channels all data' do - expect(stat.channels.peak).to eql(50) - expect(stat.channels.opened).to eql(30) + it 'returns channels data' do + expect(stat.entries['channels.peak']).to eql(50) + expect(stat.entries['channels.opened']).to eql(30) end it 'returns api_requests data' do - expect(stat.api_requests.succeeded).to eql(50) - expect(stat.api_requests.failed).to eql(10) + expect(stat.entries['apiRequests.all.succeeded']).to eql(110) # 50 apiRequests + 60 tokenRequests + expect(stat.entries['apiRequests.all.failed']).to eql(30) # 10 apiRequests + 20 tokenRequests end it 'returns token_requests data' do - expect(stat.token_requests.succeeded).to eql(60) - expect(stat.token_requests.failed).to eql(20) + expect(stat.entries['apiRequests.tokenRequests.succeeded']).to eql(60) + expect(stat.entries['apiRequests.tokenRequests.failed']).to eql(20) end - it 'returns stat objects with #interval_granularity equal to :minute' do - expect(stat.interval_granularity).to eq(:minute) + it 'returns stat objects with #unit equal to minute' do + expect(stat.unit).to eq('minute') end it 'returns stat objects with #interval_id matching :start' do @@ -145,14 +149,14 @@ let(:stat) { subject.items.first } it 'returns the first interval stats as stats are provided forwards from :start' do - expect(stat.inbound.realtime.all.count).to eql(first_inbound_realtime_count) + expect(stat.entries['messages.inbound.realtime.all.count']).to eql(first_inbound_realtime_count) end it 'returns 3 pages of stats' do expect(subject).to_not be_last page3 = subject.next.next expect(page3).to be_last - expect(page3.items.first.inbound.realtime.all.count).to eql(last_inbound_realtime_count) + expect(page3.items.first.entries['messages.inbound.realtime.all.count']).to eql(last_inbound_realtime_count) end end @@ -161,13 +165,13 @@ let(:stat) { subject.items.first } it 'returns the 3rd interval stats first as stats are provided backwards from :end' do - expect(stat.inbound.realtime.all.count).to eql(last_inbound_realtime_count) + expect(stat.entries['messages.inbound.realtime.all.count']).to eql(last_inbound_realtime_count) end it 'returns 3 pages of stats' do expect(subject).to_not be_last page3 = subject.next.next - expect(page3.items.first.inbound.realtime.all.count).to eql(first_inbound_realtime_count) + expect(page3.items.first.entries['messages.inbound.realtime.all.count']).to eql(first_inbound_realtime_count) end end @@ -177,8 +181,8 @@ context 'the REST API' do it 'defaults to direction :backwards' do - expect(stats.first.inbound.realtime.messages.count).to eql(70) # current minute - expect(stats.last.inbound.realtime.messages.count).to eql(50) # 2 minutes back + expect(stats.first.entries['messages.inbound.realtime.messages.count']).to eql(70) # current minute + expect(stats.last.entries['messages.inbound.realtime.messages.count']).to eql(50) # 2 minutes back end end end @@ -215,8 +219,8 @@ it 'should aggregate the stats for that period' do expect(subject.items.count).to eql(1) - expect(stat.all.messages.count).to eql(aggregate_messages_count) - expect(stat.all.messages.data).to eql(aggregate_messages_data) + expect(stat.entries['messages.all.messages.count']).to eql(aggregate_messages_count) + expect(stat.entries['messages.all.messages.data']).to eql(aggregate_messages_data) end end end diff --git a/spec/unit/models/stats_spec.rb b/spec/unit/models/stats_spec.rb index f2304163a..657dc2cb8 100644 --- a/spec/unit/models/stats_spec.rb +++ b/spec/unit/models/stats_spec.rb @@ -7,199 +7,64 @@ subject { Ably::Models::Stats } - %w(all persisted).each do |attribute| - context "##{attribute} stats" do - let(:data) do - { attribute.to_sym => { messages: { count: 5 }, all: { data: 10 } } } - end - subject { Ably::Models::Stats.new(data.merge(interval_id: '2004-02')).public_send(attribute) } - - it 'returns a MessageTypes object' do - expect(subject).to be_a(Ably::Models::Stats::MessageTypes) - end - - it 'returns value for message counts' do - expect(subject.messages.count).to eql(5) - end - - it 'returns value for all data transferred' do - expect(subject.all.data).to eql(10) - end - - it 'returns zero for empty values' do - expect(subject.presence.count).to eql(0) - end - - it 'raises an exception for unknown attributes' do - expect { subject.unknown }.to raise_error NoMethodError - end - - %w(all presence messages).each do |type| - context "##{type}" do - it 'is a MessageCount object' do - expect(subject.public_send(type)).to be_a(Ably::Models::Stats::MessageCount) - end - end - end + describe '#interval_id' do + it 'returns the interval ID string' do + stat = subject.new(interval_id: '2024-02-03:15:05', unit: 'minute', entries: {}, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json') + expect(stat.interval_id).to eql('2024-02-03:15:05') end end - %w(inbound outbound).each do |direction| - context "##{direction} stats" do - let(:data) do - { - direction.to_sym => { - realtime: { messages: { count: 5 }, presence: { data: 10 } }, - all: { messages: { count: 25 }, presence: { data: 210 } } - } - } - end - subject { Ably::Models::Stats.new(data.merge(interval_id: '2004-02')).public_send(direction) } - - it 'returns a MessageTraffic object' do - expect(subject).to be_a(Ably::Models::Stats::MessageTraffic) - end - - it 'returns value for realtime message counts' do - expect(subject.realtime.messages.count).to eql(5) - end - - it 'returns value for all presence data' do - expect(subject.all.presence.data).to eql(210) - end - - it 'raises an exception for unknown attributes' do - expect { subject.unknown }.to raise_error NoMethodError - end - - %w(realtime rest webhook all).each do |type| - context "##{type}" do - it 'is a MessageTypes object' do - expect(subject.public_send(type)).to be_a(Ably::Models::Stats::MessageTypes) - end - end - end + describe '#interval_time' do + it 'returns a Time object representing the start of the interval' do + stat = subject.new(interval_id: '2004-02-01:05:06', unit: 'minute', entries: {}, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json') + expect(stat.interval_time.to_i).to eql(Time.new(2004, 02, 01, 05, 06, 00, '+00:00').to_i) end end - context '#connections stats' do - let(:data) do - { connections: { tls: { opened: 5 }, all: { peak: 10 } } } - end - subject { Ably::Models::Stats.new(data.merge(interval_id: '2004-02')).connections } - - it 'returns a ConnectionTypes object' do - expect(subject).to be_a(Ably::Models::Stats::ConnectionTypes) - end - - it 'returns value for tls opened counts' do - expect(subject.tls.opened).to eql(5) - end - - it 'returns value for all peak connections' do - expect(subject.all.peak).to eql(10) - end - - it 'returns zero for empty values' do - expect(subject.all.refused).to eql(0) - end - - it 'raises an exception for unknown attributes' do - expect { subject.unknown }.to raise_error NoMethodError - end - - %w(tls plain all).each do |type| - context "##{type}" do - it 'is a ResourceCount object' do - expect(subject.public_send(type)).to be_a(Ably::Models::Stats::ResourceCount) - end - end + describe '#unit' do + it 'returns the unit from the JSON response' do + stat = subject.new(interval_id: '2024-02', unit: 'month', entries: {}, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json') + expect(stat.unit).to eql('month') end end - context '#channels stats' do - let(:data) do - { channels: { opened: 5, peak: 10 } } - end - subject { Ably::Models::Stats.new(data.merge(interval_id: '2004-02')).channels } - - it 'returns a ResourceCount object' do - expect(subject).to be_a(Ably::Models::Stats::ResourceCount) + describe '#entries' do + it 'returns the entries hash' do + entries = { 'messages.all.all.count' => 100, 'channels.peak' => 50 } + stat = subject.new(interval_id: '2024-02', unit: 'month', entries: entries, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json') + expect(stat.entries['messages.all.all.count']).to eql(100) + expect(stat.entries['channels.peak']).to eql(50) end - it 'returns value for opened counts' do - expect(subject.opened).to eql(5) - end - - it 'returns value for peak channels' do - expect(subject.peak).to eql(10) - end - - it 'returns zero for empty values' do - expect(subject.refused).to eql(0) - end - - it 'raises an exception for unknown attributes' do - expect { subject.unknown }.to raise_error NoMethodError - end - - %w(opened peak mean min refused).each do |type| - context "##{type}" do - it 'is a Integer object' do - expect(subject.public_send(type)).to be_a(Integer) - end - end + it 'returns an empty hash when entries is not present' do + stat = subject.new(interval_id: '2024-02', unit: 'month', schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json') + expect(stat.entries).to eql({}) end end - %w(api_requests token_requests).each do |request_type| - context "##{request_type} stats" do - let(:data) do - { - request_type.to_sym => { succeeded: 5, failed: 10 } - } - end - subject { Ably::Models::Stats.new(data.merge(interval_id: '2004-02')).public_send(request_type) } - - it 'returns a RequestCount object' do - expect(subject).to be_a(Ably::Models::Stats::RequestCount) - end - - it 'returns value for succeeded' do - expect(subject.succeeded).to eql(5) - end - - it 'returns value for failed' do - expect(subject.failed).to eql(10) - end - - it 'raises an exception for unknown attributes' do - expect { subject.unknown }.to raise_error NoMethodError - end + describe '#in_progress' do + it 'returns the in_progress string when present' do + stat = subject.new(interval_id: '2024-02', unit: 'month', entries: {}, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json', inProgress: '2024-02-15:10:30') + expect(stat.in_progress).to eql('2024-02-15:10:30') + end - %w(succeeded failed refused).each do |type| - context "##{type}" do - it 'is a Integer object' do - expect(subject.public_send(type)).to be_a(Integer) - end - end - end + it 'returns nil when not present' do + stat = subject.new(interval_id: '2024-02', unit: 'month', entries: {}, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json') + expect(stat.in_progress).to be_nil end end - describe '#interval_granularity' do - subject { Ably::Models::Stats.new(interval_id: '2004-02') } - - it 'returns the granularity of the interval_id' do - expect(subject.interval_granularity).to eq(:month) + describe '#schema' do + it 'returns the schema URI' do + stat = subject.new(interval_id: '2024-02', unit: 'month', entries: {}, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json') + expect(stat.schema).to eql('https://schemas.ably.com/json/app-stats-0.0.5.json') end end - describe '#interval_time' do - subject { Ably::Models::Stats.new(interval_id: '2004-02-01:05:06') } - - it 'returns a Time object representing the start of the interval' do - expect(subject.interval_time.to_i).to eql(Time.new(2004, 02, 01, 05, 06, 00, '+00:00').to_i) + describe '#app_id' do + it 'returns the application ID' do + stat = subject.new(interval_id: '2024-02', unit: 'month', entries: {}, schema: 'https://schemas.ably.com/json/app-stats-0.0.5.json', appId: 'app123') + expect(stat.app_id).to eql('app123') end end From 5e4c7658c05d2ce0a5d7da5ebe563b7ffc402853 Mon Sep 17 00:00:00 2001 From: matt423 Date: Fri, 27 Feb 2026 21:42:30 +0000 Subject: [PATCH 6/8] Bump version to 1.3.0 New public API: publish returns PublishResult (breaking for Realtime), update_message, delete_message, append_message on REST and Realtime channels. --- CHANGELOG.md | 24 ++ SPEC.md | 868 +++++++++++++++++++++++--------------------- lib/ably/version.rb | 2 +- 3 files changed, 488 insertions(+), 406 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0032beef5..d51c260e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Change Log +## [v1.3.0](https://github.com/ably/ably-ruby/tree/v1.3.0) + +[Full Changelog](https://github.com/ably/ably-ruby/compare/v1.2.8...v1.3.0) + +**Implemented enhancements:** + +* Add `update_message` for REST and Realtime channels (RSL15, RTL32) +* Add `delete_message` for REST and Realtime channels (RSL15, RTL32) +* Add `append_message` for REST and Realtime channels (RSL15, RTL32) +* Add `PublishResult` model returned from `publish` with message serials (RSL1n, PBR2a) +* Add `UpdateDeleteResult` model returned from update/delete/append with version serial (UDR2a) +* Add `MessageOperation` model for operation metadata on update/delete/append (MOP) +* Complete `Message::ACTION` enum with all TM5 values (message_create, message_update, message_delete, meta, message_summary, message_append) +* Rewrite `Stats` model to match Ably spec v2.1+ (TS12): flat `entries` hash replaces nested accessors +* Upgrade protocol version from 2 to 5 + +**Breaking changes:** + +* `Rest::Channel#publish` now returns `PublishResult` instead of `Boolean`. +* `Realtime::Channel#publish` deferrable now yields `PublishResult` instead of `Message` or `Array`. Code relying on the resolved value being a `Message` will need updating. +* `Stats` model rewritten: nested accessors (`all`, `inbound`, `outbound`, `persisted`, `connections`, `channels`, `api_requests`, `token_requests`) replaced by flat `entries` hash (TS12r). Use `stat.entries['messages.all.all.count']` instead of `stat.all.all.count`. +* `Stats::MessageTraffic`, `Stats::MessageTypes`, `Stats::MessageCount`, `Stats::ConnectionTypes`, `Stats::RequestCount`, `Stats::ResourceCount` removed. +* `Stats#interval_granularity` replaced by `Stats#unit` (TS12c). + ## [v1.2.8](https://github.com/ably/ably-ruby/tree/v1.2.8) [Full Changelog](https://github.com/ably/ably-ruby/compare/v1.2.7...v1.2.8) diff --git a/SPEC.md b/SPEC.md index 71c8b89bd..42d84a89e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ably Realtime & REST Client Library 1.2.8 Specification +# Ably Realtime & REST Client Library 1.3.0 Specification ### Ably::Realtime::Auth _(see [spec/acceptance/realtime/auth_spec.rb](./spec/acceptance/realtime/auth_spec.rb))_ @@ -1017,519 +1017,519 @@ _(see [spec/acceptance/realtime/message_spec.rb](./spec/acceptance/realtime/mess * [sends a String data payload](./spec/acceptance/realtime/message_spec.rb#L25) * with supported data payload content type * JSON Object (Hash) - * [is encoded and decoded to the same hash](./spec/acceptance/realtime/message_spec.rb#L48) + * [is encoded and decoded to the same hash](./spec/acceptance/realtime/message_spec.rb#L49) * JSON Array - * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L56) + * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L57) * String - * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L64) + * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L65) * Binary - * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L72) + * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L73) * a single Message object (#RSL1a) - * [publishes the message](./spec/acceptance/realtime/message_spec.rb#L83) + * [publishes the message](./spec/acceptance/realtime/message_spec.rb#L84) * an array of Message objects (#RSL1a) - * [publishes three messages](./spec/acceptance/realtime/message_spec.rb#L100) + * [publishes three messages](./spec/acceptance/realtime/message_spec.rb#L101) * an array of hashes (#RSL1a) - * [publishes three messages](./spec/acceptance/realtime/message_spec.rb#L123) + * [publishes three messages](./spec/acceptance/realtime/message_spec.rb#L124) * a name with data payload (#RSL1a, #RSL1b) - * [publishes a message](./spec/acceptance/realtime/message_spec.rb#L144) + * [publishes a message](./spec/acceptance/realtime/message_spec.rb#L145) * with supported extra payload content type (#RTL6h, #RSL6a2) * JSON Object (Hash) - * [is encoded and decoded to the same hash](./spec/acceptance/realtime/message_spec.rb#L170) + * [is encoded and decoded to the same hash](./spec/acceptance/realtime/message_spec.rb#L171) * JSON Array - * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L178) + * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L179) * nil - * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L184) + * [is encoded and decoded to the same Array](./spec/acceptance/realtime/message_spec.rb#L185) * with unsupported data payload content type * Integer - * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L195) + * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L196) * Float - * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L204) + * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L205) * Boolean - * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L213) + * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L214) * False - * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L222) + * [is raises an UnsupportedDataType 40013 exception](./spec/acceptance/realtime/message_spec.rb#L223) * with ASCII_8BIT message name - * [is converted into UTF_8](./spec/acceptance/realtime/message_spec.rb#L231) + * [is converted into UTF_8](./spec/acceptance/realtime/message_spec.rb#L232) * when the message publisher has a client_id - * [contains a #client_id attribute](./spec/acceptance/realtime/message_spec.rb#L247) + * [contains a #client_id attribute](./spec/acceptance/realtime/message_spec.rb#L248) * #connection_id attribute * over realtime - * [matches the sender connection#id](./spec/acceptance/realtime/message_spec.rb#L260) + * [matches the sender connection#id](./spec/acceptance/realtime/message_spec.rb#L261) * when retrieved over REST - * [matches the sender connection#id](./spec/acceptance/realtime/message_spec.rb#L272) + * [matches the sender connection#id](./spec/acceptance/realtime/message_spec.rb#L273) * local echo when published - * [is enabled by default](./spec/acceptance/realtime/message_spec.rb#L284) + * [is enabled by default](./spec/acceptance/realtime/message_spec.rb#L285) * with :echo_messages option set to false - * [will not echo messages to the client but will still broadcast messages to other connected clients](./spec/acceptance/realtime/message_spec.rb#L304) - * [will not echo messages to the client from other REST clients publishing using that connection_key](./spec/acceptance/realtime/message_spec.rb#L322) - * [will echo messages with a valid connection_id to the client from other REST clients publishing using that connection_key](./spec/acceptance/realtime/message_spec.rb#L335) + * [will not echo messages to the client but will still broadcast messages to other connected clients](./spec/acceptance/realtime/message_spec.rb#L305) + * [will not echo messages to the client from other REST clients publishing using that connection_key](./spec/acceptance/realtime/message_spec.rb#L323) + * [will echo messages with a valid connection_id to the client from other REST clients publishing using that connection_key](./spec/acceptance/realtime/message_spec.rb#L336) * publishing lots of messages across two connections - * [sends and receives the messages on both opened connections and calls the success callbacks for each message published](./spec/acceptance/realtime/message_spec.rb#L361) + * [sends and receives the messages on both opened connections and calls the success callbacks for each message published](./spec/acceptance/realtime/message_spec.rb#L362) * without suitable publishing permissions - * [calls the error callback](./spec/acceptance/realtime/message_spec.rb#L406) + * [calls the error callback](./spec/acceptance/realtime/message_spec.rb#L407) * encoding and decoding encrypted messages * with AES-128-CBC using crypto-data-128.json fixtures (#RTL7d) * item 0 with encrypted encoding utf-8/cipher+aes-128-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 1 with encrypted encoding cipher+aes-128-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 2 with encrypted encoding json/utf-8/cipher+aes-128-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 3 with encrypted encoding json/utf-8/cipher+aes-128-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * with AES-256-CBC using crypto-data-256.json fixtures (#RTL7d) * item 0 with encrypted encoding utf-8/cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 1 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 2 with encrypted encoding json/utf-8/cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 3 with encrypted encoding json/utf-8/cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 4 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 5 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 6 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 7 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 8 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 9 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 10 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 11 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 12 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 13 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 14 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 15 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 16 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 17 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 18 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 19 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 20 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 21 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 22 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 23 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 24 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 25 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 26 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 27 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 28 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 29 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 30 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 31 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 32 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 33 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 34 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 35 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 36 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 37 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 38 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 39 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 40 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 41 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 42 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 43 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 44 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 45 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 46 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 47 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 48 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 49 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 50 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 51 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 52 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 53 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 54 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 55 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 56 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 57 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 58 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 59 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 60 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 61 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 62 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 63 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 64 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 65 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 66 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 67 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 68 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 69 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 70 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 71 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 72 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * item 73 with encrypted encoding cipher+aes-256-cbc/base64 * behaves like an Ably encrypter and decrypter * with #publish and #subscribe - * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L457) - * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L477) + * [encrypts message automatically before they are pushed to the server (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L458) + * [sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)](./spec/acceptance/realtime/message_spec.rb#L478) * with multiple sends from one client to another - * [encrypts and decrypts all messages](./spec/acceptance/realtime/message_spec.rb#L516) - * [receives raw messages with the correct encoding](./spec/acceptance/realtime/message_spec.rb#L533) + * [encrypts and decrypts all messages](./spec/acceptance/realtime/message_spec.rb#L517) + * [receives raw messages with the correct encoding](./spec/acceptance/realtime/message_spec.rb#L534) * subscribing with a different transport protocol - * [delivers a String ASCII-8BIT payload to the receiver](./spec/acceptance/realtime/message_spec.rb#L567) - * [delivers a String UTF-8 payload to the receiver](./spec/acceptance/realtime/message_spec.rb#L567) - * [delivers a Hash payload to the receiver](./spec/acceptance/realtime/message_spec.rb#L567) + * [delivers a String ASCII-8BIT payload to the receiver](./spec/acceptance/realtime/message_spec.rb#L568) + * [delivers a String UTF-8 payload to the receiver](./spec/acceptance/realtime/message_spec.rb#L568) + * [delivers a Hash payload to the receiver](./spec/acceptance/realtime/message_spec.rb#L568) * publishing on an unencrypted channel and subscribing on an encrypted channel with another client - * [does not attempt to decrypt the message](./spec/acceptance/realtime/message_spec.rb#L588) + * [does not attempt to decrypt the message](./spec/acceptance/realtime/message_spec.rb#L589) * publishing on an encrypted channel and subscribing on an unencrypted channel with another client - * [delivers the message but still encrypted with a value in the #encoding attribute (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L608) - * [logs a Cipher error (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L619) + * [delivers the message but still encrypted with a value in the #encoding attribute (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L609) + * [logs a Cipher error (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L620) * publishing on an encrypted channel and subscribing with a different algorithm on another client - * [delivers the message but still encrypted with the cipher detials in the #encoding attribute (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L639) - * [emits a Cipher error on the channel (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L650) + * [delivers the message but still encrypted with the cipher detials in the #encoding attribute (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L640) + * [emits a Cipher error on the channel (#RTL7e)](./spec/acceptance/realtime/message_spec.rb#L651) * publishing on an encrypted channel and subscribing with a different key on another client - * [delivers the message but still encrypted with the cipher details in the #encoding attribute](./spec/acceptance/realtime/message_spec.rb#L670) - * [emits a Cipher error on the channel](./spec/acceptance/realtime/message_spec.rb#L681) + * [delivers the message but still encrypted with the cipher details in the #encoding attribute](./spec/acceptance/realtime/message_spec.rb#L671) + * [emits a Cipher error on the channel](./spec/acceptance/realtime/message_spec.rb#L682) * when message is published, the connection disconnects before the ACK is received, and the connection is resumed - * [publishes the message again, later receives the ACK and only one message is ever received from Ably](./spec/acceptance/realtime/message_spec.rb#L700) + * [publishes the message again, later receives the ACK and only one message is ever received from Ably](./spec/acceptance/realtime/message_spec.rb#L701) * when message is published, the connection disconnects before the ACK is received * the connection is not resumed - * [calls the errback for all messages](./spec/acceptance/realtime/message_spec.rb#L745) + * [calls the errback for all messages](./spec/acceptance/realtime/message_spec.rb#L746) * the connection becomes suspended - * [calls the errback for all messages](./spec/acceptance/realtime/message_spec.rb#L771) + * [calls the errback for all messages](./spec/acceptance/realtime/message_spec.rb#L772) * the connection becomes failed - * [calls the errback for all messages](./spec/acceptance/realtime/message_spec.rb#L798) + * [calls the errback for all messages](./spec/acceptance/realtime/message_spec.rb#L799) * message encoding interoperability * over a JSON transport * when decoding string - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L839) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L840) * when encoding string - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L857) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L858) * when decoding string - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L839) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L840) * when encoding string - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L857) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L858) * when decoding jsonObject - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L839) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L840) * when encoding jsonObject - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L857) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L858) * when decoding jsonArray - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L839) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L840) * when encoding jsonArray - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L857) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L858) * when decoding binary - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L839) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L840) * when encoding binary - * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L857) + * [ensures that client libraries have compatible encoding and decoding using common fixtures](./spec/acceptance/realtime/message_spec.rb#L858) * over a MsgPack transport * when publishing a string using JSON protocol - * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L891) + * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L892) * when retrieving a string using JSON protocol - * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L919) + * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L920) * when publishing a string using JSON protocol - * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L891) + * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L892) * when retrieving a string using JSON protocol - * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L919) + * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L920) * when publishing a jsonObject using JSON protocol - * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L891) + * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L892) * when retrieving a jsonObject using JSON protocol - * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L919) + * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L920) * when publishing a jsonArray using JSON protocol - * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L891) + * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L892) * when retrieving a jsonArray using JSON protocol - * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L919) + * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L920) * when publishing a binary using JSON protocol - * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L891) + * [receives the message over MsgPack and the data matches](./spec/acceptance/realtime/message_spec.rb#L892) * when retrieving a binary using JSON protocol - * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L919) + * [is compatible with a publishes using MsgPack](./spec/acceptance/realtime/message_spec.rb#L920) ### Ably::Realtime::Presence history _(see [spec/acceptance/realtime/presence_history_spec.rb](./spec/acceptance/realtime/presence_history_spec.rb))_ @@ -2312,13 +2312,13 @@ _(see [spec/acceptance/rest/base_spec.rb](./spec/acceptance/rest/base_spec.rb))_ * transport protocol * when protocol is not defined it defaults to :msgpack * [uses MsgPack](./spec/acceptance/rest/base_spec.rb#L27) - * when option {:protocol=>:json} is used + * when option {protocol: :json} is used * [uses JSON](./spec/acceptance/rest/base_spec.rb#L43) - * when option {:use_binary_protocol=>false} is used + * when option {use_binary_protocol: false} is used * [uses JSON](./spec/acceptance/rest/base_spec.rb#L43) - * when option {:protocol=>:msgpack} is used + * when option {protocol: :msgpack} is used * [uses MsgPack](./spec/acceptance/rest/base_spec.rb#L60) - * when option {:use_binary_protocol=>true} is used + * when option {use_binary_protocol: true} is used * [uses MsgPack](./spec/acceptance/rest/base_spec.rb#L60) * using JSON protocol * failed requests @@ -2339,9 +2339,9 @@ _(see [spec/acceptance/rest/channel_spec.rb](./spec/acceptance/rest/channel_spec * using JSON protocol * #publish * with name and data arguments - * [publishes the message and return true indicating success](./spec/acceptance/rest/channel_spec.rb#L23) + * [publishes the message and returns a PublishResult](./spec/acceptance/rest/channel_spec.rb#L23) * and additional attributes - * [publishes the message with the attributes and return true indicating success](./spec/acceptance/rest/channel_spec.rb#L32) + * [publishes the message with the attributes and returns a PublishResult](./spec/acceptance/rest/channel_spec.rb#L32) * with a client_id configured in the ClientOptions * [publishes the message without a client_id](./spec/acceptance/rest/channel_spec.rb#L43) * [expects a client_id to be added by the realtime service](./spec/acceptance/rest/channel_spec.rb#L51) @@ -3246,42 +3246,43 @@ _(see [spec/acceptance/rest/stats_spec.rb](./spec/acceptance/rest/stats_spec.rb) * [returns a PaginatedResult object](./spec/acceptance/rest/stats_spec.rb#L54) * by minute * with no options - * [uses the minute interval by default](./spec/acceptance/rest/stats_spec.rb#L66) + * [returns the unit from the JSON response](./spec/acceptance/rest/stats_spec.rb#L66) * with :from set to last interval and :limit set to 1 * [retrieves only one stat](./spec/acceptance/rest/stats_spec.rb#L75) - * [returns zero value for any missing metrics](./spec/acceptance/rest/stats_spec.rb#L79) - * [returns all aggregated message data](./spec/acceptance/rest/stats_spec.rb#L84) - * [returns inbound realtime all data](./spec/acceptance/rest/stats_spec.rb#L89) - * [returns inbound realtime message data](./spec/acceptance/rest/stats_spec.rb#L94) - * [returns outbound realtime all data](./spec/acceptance/rest/stats_spec.rb#L99) - * [returns persisted presence all data](./spec/acceptance/rest/stats_spec.rb#L104) - * [returns connections all data](./spec/acceptance/rest/stats_spec.rb#L109) - * [returns channels all data](./spec/acceptance/rest/stats_spec.rb#L114) - * [returns api_requests data](./spec/acceptance/rest/stats_spec.rb#L119) - * [returns token_requests data](./spec/acceptance/rest/stats_spec.rb#L124) - * [returns stat objects with #interval_granularity equal to :minute](./spec/acceptance/rest/stats_spec.rb#L129) - * [returns stat objects with #interval_id matching :start](./spec/acceptance/rest/stats_spec.rb#L133) - * [returns stat objects with #interval_time matching :start Time](./spec/acceptance/rest/stats_spec.rb#L137) + * [returns entries as a flat hash (#TS12r)](./spec/acceptance/rest/stats_spec.rb#L79) + * [returns zero or nil for any missing entries](./spec/acceptance/rest/stats_spec.rb#L83) + * [returns all aggregated message data](./spec/acceptance/rest/stats_spec.rb#L88) + * [returns inbound realtime all data](./spec/acceptance/rest/stats_spec.rb#L93) + * [returns inbound realtime message data](./spec/acceptance/rest/stats_spec.rb#L98) + * [returns outbound realtime all data](./spec/acceptance/rest/stats_spec.rb#L103) + * [returns persisted presence all data](./spec/acceptance/rest/stats_spec.rb#L108) + * [returns connections all data](./spec/acceptance/rest/stats_spec.rb#L113) + * [returns channels data](./spec/acceptance/rest/stats_spec.rb#L118) + * [returns api_requests data](./spec/acceptance/rest/stats_spec.rb#L123) + * [returns token_requests data](./spec/acceptance/rest/stats_spec.rb#L128) + * [returns stat objects with #unit equal to minute](./spec/acceptance/rest/stats_spec.rb#L133) + * [returns stat objects with #interval_id matching :start](./spec/acceptance/rest/stats_spec.rb#L137) + * [returns stat objects with #interval_time matching :start Time](./spec/acceptance/rest/stats_spec.rb#L141) * with :start set to first interval, :limit set to 1 and direction :forwards - * [returns the first interval stats as stats are provided forwards from :start](./spec/acceptance/rest/stats_spec.rb#L147) - * [returns 3 pages of stats](./spec/acceptance/rest/stats_spec.rb#L151) + * [returns the first interval stats as stats are provided forwards from :start](./spec/acceptance/rest/stats_spec.rb#L151) + * [returns 3 pages of stats](./spec/acceptance/rest/stats_spec.rb#L155) * with :end set to last interval, :limit set to 1 and direction :backwards - * [returns the 3rd interval stats first as stats are provided backwards from :end](./spec/acceptance/rest/stats_spec.rb#L163) - * [returns 3 pages of stats](./spec/acceptance/rest/stats_spec.rb#L167) + * [returns the 3rd interval stats first as stats are provided backwards from :end](./spec/acceptance/rest/stats_spec.rb#L167) + * [returns 3 pages of stats](./spec/acceptance/rest/stats_spec.rb#L171) * with :end set to last interval and :limit set to 3 to ensure only last years stats are included * the REST API - * [defaults to direction :backwards](./spec/acceptance/rest/stats_spec.rb#L179) + * [defaults to direction :backwards](./spec/acceptance/rest/stats_spec.rb#L183) * with :end set to previous year interval * the REST API - * [defaults to 100 items for pagination](./spec/acceptance/rest/stats_spec.rb#L191) + * [defaults to 100 items for pagination](./spec/acceptance/rest/stats_spec.rb#L195) * by hour - * [should aggregate the stats for that period](./spec/acceptance/rest/stats_spec.rb#L215) + * [should aggregate the stats for that period](./spec/acceptance/rest/stats_spec.rb#L219) * by day - * [should aggregate the stats for that period](./spec/acceptance/rest/stats_spec.rb#L215) + * [should aggregate the stats for that period](./spec/acceptance/rest/stats_spec.rb#L219) * by month - * [should aggregate the stats for that period](./spec/acceptance/rest/stats_spec.rb#L215) + * [should aggregate the stats for that period](./spec/acceptance/rest/stats_spec.rb#L219) * when argument start is after end - * [should raise an exception](./spec/acceptance/rest/stats_spec.rb#L227) + * [should raise an exception](./spec/acceptance/rest/stats_spec.rb#L231) ### Ably::Rest::Client#time _(see [spec/acceptance/rest/time_spec.rb](./spec/acceptance/rest/time_spec.rb))_ @@ -3810,6 +3811,23 @@ _(see [spec/unit/models/message_encoders/utf8_spec.rb](./spec/unit/models/messag * [leaves the message data intact](./spec/unit/models/message_encoders/utf8_spec.rb#L47) * [leaves the encoding intact](./spec/unit/models/message_encoders/utf8_spec.rb#L51) +### Ably::Models::MessageOperation +_(see [spec/unit/models/message_operation_spec.rb](./spec/unit/models/message_operation_spec.rb))_ + * #client_id (#MOP2a) + * [returns the client_id](./spec/unit/models/message_operation_spec.rb#L10) + * #description (#MOP2b) + * [returns the description](./spec/unit/models/message_operation_spec.rb#L18) + * #metadata (#MOP2c) + * [returns the metadata hash](./spec/unit/models/message_operation_spec.rb#L26) + * when empty + * [returns nil for all fields](./spec/unit/models/message_operation_spec.rb#L34) + * with camelCase keys from wire + * [converts to snake_case access](./spec/unit/models/message_operation_spec.rb#L44) + * #attributes + * [prevents modification](./spec/unit/models/message_operation_spec.rb#L52) + * #as_json + * [returns a hash suitable for JSON serialization](./spec/unit/models/message_operation_spec.rb#L60) + ### Ably::Models::Message _(see [spec/unit/models/message_spec.rb](./spec/unit/models/message_spec.rb))_ * serialization of the Message object (#RSL1j) @@ -3938,6 +3956,47 @@ _(see [spec/unit/models/message_spec.rb](./spec/unit/models/message_spec.rb))_ * [should return 1234-1234-5678-9009 message id](./spec/unit/models/message_spec.rb#L634) * when no delta * [should return nil](./spec/unit/models/message_spec.rb#L642) + * #action (#TM2j) + * when action is present + * [returns the action as an ACTION enum](./spec/unit/models/message_spec.rb#L652) + * [can be compared with a symbol](./spec/unit/models/message_spec.rb#L656) + * [can be compared with an integer](./spec/unit/models/message_spec.rb#L660) + * when action is not present + * [returns nil](./spec/unit/models/message_spec.rb#L668) + * ACTION enum values (#TM5) + * [has message_create as 0](./spec/unit/models/message_spec.rb#L674) + * [has message_update as 1](./spec/unit/models/message_spec.rb#L678) + * [has message_delete as 2](./spec/unit/models/message_spec.rb#L682) + * [has meta as 3](./spec/unit/models/message_spec.rb#L686) + * [has message_summary as 4](./spec/unit/models/message_spec.rb#L690) + * [has message_append as 5](./spec/unit/models/message_spec.rb#L694) + * #serial (#TM2r) + * [returns the serial attribute](./spec/unit/models/message_spec.rb#L704) + * when not present + * [returns nil](./spec/unit/models/message_spec.rb#L711) + * #version (#TM2s) + * [returns the version attribute](./spec/unit/models/message_spec.rb#L721) + * when not present + * [returns nil](./spec/unit/models/message_spec.rb#L728) + * #created_at + * [returns a Time object](./spec/unit/models/message_spec.rb#L738) + * when not present + * [returns nil](./spec/unit/models/message_spec.rb#L745) + * #updated_at + * [returns a Time object](./spec/unit/models/message_spec.rb#L755) + * when not present + * [returns nil](./spec/unit/models/message_spec.rb#L762) + * #as_json + * with action + * [converts action to integer](./spec/unit/models/message_spec.rb#L772) + * [includes name](./spec/unit/models/message_spec.rb#L776) + * without action + * [does not include action key](./spec/unit/models/message_spec.rb#L784) + * with serial and version + * [includes serial](./spec/unit/models/message_spec.rb#L792) + * [includes version](./spec/unit/models/message_spec.rb#L796) + * excludes nil values + * [does not include nil attributes](./spec/unit/models/message_spec.rb#L804) ### Ably::Models::PaginatedResult _(see [spec/unit/models/paginated_result_spec.rb](./spec/unit/models/paginated_result_spec.rb))_ @@ -4153,41 +4212,68 @@ _(see [spec/unit/models/protocol_message_spec.rb](./spec/unit/models/protocol_me * when has another future flag * [#has_presence_flag? is false](./spec/unit/models/protocol_message_spec.rb#L208) * [#has_backlog_flag? is true](./spec/unit/models/protocol_message_spec.rb#L212) + * #res (#TR4s) + * when present + * [returns the res array](./spec/unit/models/protocol_message_spec.rb#L223) + * [contains publish result entries with serials](./spec/unit/models/protocol_message_spec.rb#L228) + * when absent + * [returns nil](./spec/unit/models/protocol_message_spec.rb#L237) + * with multiple entries + * [returns all entries](./spec/unit/models/protocol_message_spec.rb#L251) * #params (#RTL4k1) * when present - * [is expected to eq {:foo=>:bar}](./spec/unit/models/protocol_message_spec.rb#L224) + * [is expected to eq {:foo => :bar}](./spec/unit/models/protocol_message_spec.rb#L264) * when empty - * [is expected to eq {}](./spec/unit/models/protocol_message_spec.rb#L230) + * [is expected to eq {}](./spec/unit/models/protocol_message_spec.rb#L270) * #error * with no error attribute - * [returns nil](./spec/unit/models/protocol_message_spec.rb#L240) + * [returns nil](./spec/unit/models/protocol_message_spec.rb#L280) * with nil error - * [returns nil](./spec/unit/models/protocol_message_spec.rb#L248) + * [returns nil](./spec/unit/models/protocol_message_spec.rb#L288) * with error - * [returns a valid ErrorInfo object](./spec/unit/models/protocol_message_spec.rb#L256) + * [returns a valid ErrorInfo object](./spec/unit/models/protocol_message_spec.rb#L296) * #messages (#TR4k) - * [contains Message objects](./spec/unit/models/protocol_message_spec.rb#L266) + * [contains Message objects](./spec/unit/models/protocol_message_spec.rb#L306) * #messages (#RTL21) - * [contains Message objects in ascending order](./spec/unit/models/protocol_message_spec.rb#L284) + * [contains Message objects in ascending order](./spec/unit/models/protocol_message_spec.rb#L324) * #presence (#TR4l) - * [contains PresenceMessage objects](./spec/unit/models/protocol_message_spec.rb#L296) + * [contains PresenceMessage objects](./spec/unit/models/protocol_message_spec.rb#L336) * #message_size (#TO3l8) * on presence - * [should return 13 bytes (sum in bytes: data and client_id)](./spec/unit/models/protocol_message_spec.rb#L309) + * [should return 13 bytes (sum in bytes: data and client_id)](./spec/unit/models/protocol_message_spec.rb#L349) * on message - * [should return 76 bytes (sum in bytes: data, client_id, name, extras)](./spec/unit/models/protocol_message_spec.rb#L319) + * [should return 76 bytes (sum in bytes: data, client_id, name, extras)](./spec/unit/models/protocol_message_spec.rb#L359) * #connection_details (#TR4o) * with a JSON value - * [contains a ConnectionDetails object](./spec/unit/models/protocol_message_spec.rb#L331) - * [contains the attributes from the JSON connectionDetails](./spec/unit/models/protocol_message_spec.rb#L335) + * [contains a ConnectionDetails object](./spec/unit/models/protocol_message_spec.rb#L371) + * [contains the attributes from the JSON connectionDetails](./spec/unit/models/protocol_message_spec.rb#L375) * without a JSON value - * [contains an empty ConnectionDetails object](./spec/unit/models/protocol_message_spec.rb#L344) + * [contains an empty ConnectionDetails object](./spec/unit/models/protocol_message_spec.rb#L384) * #auth (#TR4p) * with a JSON value - * [contains a AuthDetails object](./spec/unit/models/protocol_message_spec.rb#L358) - * [contains the attributes from the JSON auth details](./spec/unit/models/protocol_message_spec.rb#L362) + * [contains a AuthDetails object](./spec/unit/models/protocol_message_spec.rb#L398) + * [contains the attributes from the JSON auth details](./spec/unit/models/protocol_message_spec.rb#L402) * without a JSON value - * [contains an empty AuthDetails object](./spec/unit/models/protocol_message_spec.rb#L370) + * [contains an empty AuthDetails object](./spec/unit/models/protocol_message_spec.rb#L410) + +### Ably::Models::PublishResult +_(see [spec/unit/models/publish_result_spec.rb](./spec/unit/models/publish_result_spec.rb))_ + * #serials (#RSL1n) + * when present + * [returns the serials array](./spec/unit/models/publish_result_spec.rb#L11) + * with nullable entries + * [preserves nil entries](./spec/unit/models/publish_result_spec.rb#L19) + * when empty array + * [returns empty array](./spec/unit/models/publish_result_spec.rb#L27) + * when nil + * [returns empty array](./spec/unit/models/publish_result_spec.rb#L35) + * when not provided + * [returns empty array](./spec/unit/models/publish_result_spec.rb#L43) + * #attributes + * [returns the underlying attributes](./spec/unit/models/publish_result_spec.rb#L52) + * [prevents modification](./spec/unit/models/publish_result_spec.rb#L56) + * truthiness + * [is truthy for backward compatibility with boolean publish returns](./spec/unit/models/publish_result_spec.rb#L64) ### Ably::Models::PushChannelSubscription _(see [spec/unit/models/push_channel_subscription_spec.rb](./spec/unit/models/push_channel_subscription_spec.rb))_ @@ -4216,133 +4302,45 @@ _(see [spec/unit/models/push_channel_subscription_spec.rb](./spec/unit/models/pu ### Ably::Models::Stats _(see [spec/unit/models/stats_spec.rb](./spec/unit/models/stats_spec.rb))_ - * #all stats - * [returns a MessageTypes object](./spec/unit/models/stats_spec.rb#L17) - * [returns value for message counts](./spec/unit/models/stats_spec.rb#L21) - * [returns value for all data transferred](./spec/unit/models/stats_spec.rb#L25) - * [returns zero for empty values](./spec/unit/models/stats_spec.rb#L29) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L33) - * #all - * [is a MessageCount object](./spec/unit/models/stats_spec.rb#L39) - * #presence - * [is a MessageCount object](./spec/unit/models/stats_spec.rb#L39) - * #messages - * [is a MessageCount object](./spec/unit/models/stats_spec.rb#L39) - * #persisted stats - * [returns a MessageTypes object](./spec/unit/models/stats_spec.rb#L17) - * [returns value for message counts](./spec/unit/models/stats_spec.rb#L21) - * [returns value for all data transferred](./spec/unit/models/stats_spec.rb#L25) - * [returns zero for empty values](./spec/unit/models/stats_spec.rb#L29) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L33) - * #all - * [is a MessageCount object](./spec/unit/models/stats_spec.rb#L39) - * #presence - * [is a MessageCount object](./spec/unit/models/stats_spec.rb#L39) - * #messages - * [is a MessageCount object](./spec/unit/models/stats_spec.rb#L39) - * #inbound stats - * [returns a MessageTraffic object](./spec/unit/models/stats_spec.rb#L59) - * [returns value for realtime message counts](./spec/unit/models/stats_spec.rb#L63) - * [returns value for all presence data](./spec/unit/models/stats_spec.rb#L67) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L71) - * #realtime - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #rest - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #webhook - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #all - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #outbound stats - * [returns a MessageTraffic object](./spec/unit/models/stats_spec.rb#L59) - * [returns value for realtime message counts](./spec/unit/models/stats_spec.rb#L63) - * [returns value for all presence data](./spec/unit/models/stats_spec.rb#L67) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L71) - * #realtime - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #rest - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #webhook - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #all - * [is a MessageTypes object](./spec/unit/models/stats_spec.rb#L77) - * #connections stats - * [returns a ConnectionTypes object](./spec/unit/models/stats_spec.rb#L91) - * [returns value for tls opened counts](./spec/unit/models/stats_spec.rb#L95) - * [returns value for all peak connections](./spec/unit/models/stats_spec.rb#L99) - * [returns zero for empty values](./spec/unit/models/stats_spec.rb#L103) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L107) - * #tls - * [is a ResourceCount object](./spec/unit/models/stats_spec.rb#L113) - * #plain - * [is a ResourceCount object](./spec/unit/models/stats_spec.rb#L113) - * #all - * [is a ResourceCount object](./spec/unit/models/stats_spec.rb#L113) - * #channels stats - * [returns a ResourceCount object](./spec/unit/models/stats_spec.rb#L126) - * [returns value for opened counts](./spec/unit/models/stats_spec.rb#L130) - * [returns value for peak channels](./spec/unit/models/stats_spec.rb#L134) - * [returns zero for empty values](./spec/unit/models/stats_spec.rb#L138) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L142) - * #opened - * [is a Integer object](./spec/unit/models/stats_spec.rb#L148) - * #peak - * [is a Integer object](./spec/unit/models/stats_spec.rb#L148) - * #mean - * [is a Integer object](./spec/unit/models/stats_spec.rb#L148) - * #min - * [is a Integer object](./spec/unit/models/stats_spec.rb#L148) - * #refused - * [is a Integer object](./spec/unit/models/stats_spec.rb#L148) - * #api_requests stats - * [returns a RequestCount object](./spec/unit/models/stats_spec.rb#L164) - * [returns value for succeeded](./spec/unit/models/stats_spec.rb#L168) - * [returns value for failed](./spec/unit/models/stats_spec.rb#L172) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L176) - * #succeeded - * [is a Integer object](./spec/unit/models/stats_spec.rb#L182) - * #failed - * [is a Integer object](./spec/unit/models/stats_spec.rb#L182) - * #refused - * [is a Integer object](./spec/unit/models/stats_spec.rb#L182) - * #token_requests stats - * [returns a RequestCount object](./spec/unit/models/stats_spec.rb#L164) - * [returns value for succeeded](./spec/unit/models/stats_spec.rb#L168) - * [returns value for failed](./spec/unit/models/stats_spec.rb#L172) - * [raises an exception for unknown attributes](./spec/unit/models/stats_spec.rb#L176) - * #succeeded - * [is a Integer object](./spec/unit/models/stats_spec.rb#L182) - * #failed - * [is a Integer object](./spec/unit/models/stats_spec.rb#L182) - * #refused - * [is a Integer object](./spec/unit/models/stats_spec.rb#L182) - * #interval_granularity - * [returns the granularity of the interval_id](./spec/unit/models/stats_spec.rb#L193) + * #interval_id + * [returns the interval ID string](./spec/unit/models/stats_spec.rb#L11) * #interval_time - * [returns a Time object representing the start of the interval](./spec/unit/models/stats_spec.rb#L201) + * [returns a Time object representing the start of the interval](./spec/unit/models/stats_spec.rb#L18) + * #unit + * [returns the unit from the JSON response](./spec/unit/models/stats_spec.rb#L25) + * #entries + * [returns the entries hash](./spec/unit/models/stats_spec.rb#L32) + * [returns an empty hash when entries is not present](./spec/unit/models/stats_spec.rb#L39) + * #in_progress + * [returns the in_progress string when present](./spec/unit/models/stats_spec.rb#L46) + * [returns nil when not present](./spec/unit/models/stats_spec.rb#L51) + * #schema + * [returns the schema URI](./spec/unit/models/stats_spec.rb#L58) + * #app_id + * [returns the application ID](./spec/unit/models/stats_spec.rb#L65) * class methods * #to_interval_id * when time zone of time argument is UTC - * [converts time 2014-02-03:05:06 with granularity :month into 2014-02](./spec/unit/models/stats_spec.rb#L209) - * [converts time 2014-02-03:05:06 with granularity :day into 2014-02-03](./spec/unit/models/stats_spec.rb#L213) - * [converts time 2014-02-03:05:06 with granularity :hour into 2014-02-03:05](./spec/unit/models/stats_spec.rb#L217) - * [converts time 2014-02-03:05:06 with granularity :minute into 2014-02-03:05:06](./spec/unit/models/stats_spec.rb#L221) - * [fails with invalid granularity](./spec/unit/models/stats_spec.rb#L225) - * [fails with invalid time](./spec/unit/models/stats_spec.rb#L229) + * [converts time 2014-02-03:05:06 with granularity :month into 2014-02](./spec/unit/models/stats_spec.rb#L74) + * [converts time 2014-02-03:05:06 with granularity :day into 2014-02-03](./spec/unit/models/stats_spec.rb#L78) + * [converts time 2014-02-03:05:06 with granularity :hour into 2014-02-03:05](./spec/unit/models/stats_spec.rb#L82) + * [converts time 2014-02-03:05:06 with granularity :minute into 2014-02-03:05:06](./spec/unit/models/stats_spec.rb#L86) + * [fails with invalid granularity](./spec/unit/models/stats_spec.rb#L90) + * [fails with invalid time](./spec/unit/models/stats_spec.rb#L94) * when time zone of time argument is +02:00 - * [converts time 2014-02-03:06 with granularity :hour into 2014-02-03:04 at UTC +00:00](./spec/unit/models/stats_spec.rb#L235) + * [converts time 2014-02-03:06 with granularity :hour into 2014-02-03:04 at UTC +00:00](./spec/unit/models/stats_spec.rb#L100) * #from_interval_id - * [converts a month interval_id 2014-02 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L242) - * [converts a day interval_id 2014-02-03 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L247) - * [converts an hour interval_id 2014-02-03:05 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L252) - * [converts a minute interval_id 2014-02-03:05:06 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L257) - * [fails with an invalid interval_id 14-20](./spec/unit/models/stats_spec.rb#L262) + * [converts a month interval_id 2014-02 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L107) + * [converts a day interval_id 2014-02-03 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L112) + * [converts an hour interval_id 2014-02-03:05 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L117) + * [converts a minute interval_id 2014-02-03:05:06 into a Time object in UTC 0](./spec/unit/models/stats_spec.rb#L122) + * [fails with an invalid interval_id 14-20](./spec/unit/models/stats_spec.rb#L127) * #granularity_from_interval_id - * [returns a :month interval_id for 2014-02](./spec/unit/models/stats_spec.rb#L268) - * [returns a :day interval_id for 2014-02-03](./spec/unit/models/stats_spec.rb#L272) - * [returns a :hour interval_id for 2014-02-03:05](./spec/unit/models/stats_spec.rb#L276) - * [returns a :minute interval_id for 2014-02-03:05:06](./spec/unit/models/stats_spec.rb#L280) - * [fails with an invalid interval_id 14-20](./spec/unit/models/stats_spec.rb#L284) + * [returns a :month interval_id for 2014-02](./spec/unit/models/stats_spec.rb#L133) + * [returns a :day interval_id for 2014-02-03](./spec/unit/models/stats_spec.rb#L137) + * [returns a :hour interval_id for 2014-02-03:05](./spec/unit/models/stats_spec.rb#L141) + * [returns a :minute interval_id for 2014-02-03:05:06](./spec/unit/models/stats_spec.rb#L145) + * [fails with an invalid interval_id 14-20](./spec/unit/models/stats_spec.rb#L149) ### Ably::Models::TokenDetails _(see [spec/unit/models/token_details_spec.rb](./spec/unit/models/token_details_spec.rb))_ @@ -4450,6 +4448,21 @@ _(see [spec/unit/models/token_request_spec.rb](./spec/unit/models/token_request_ * with JSON string * [returns a valid TokenRequest object](./spec/unit/models/token_request_spec.rb#L174) +### Ably::Models::UpdateDeleteResult +_(see [spec/unit/models/update_delete_result_spec.rb](./spec/unit/models/update_delete_result_spec.rb))_ + * #version_serial (#UDR2a) + * when present + * [returns the version serial](./spec/unit/models/update_delete_result_spec.rb#L11) + * when nil + * [returns nil](./spec/unit/models/update_delete_result_spec.rb#L19) + * when not provided + * [returns nil](./spec/unit/models/update_delete_result_spec.rb#L27) + * with camelCase keys from wire + * [converts to snake_case access](./spec/unit/models/update_delete_result_spec.rb#L36) + * #attributes + * [returns the underlying attributes](./spec/unit/models/update_delete_result_spec.rb#L44) + * [prevents modification](./spec/unit/models/update_delete_result_spec.rb#L48) + ### Ably::Modules::EventEmitter _(see [spec/unit/modules/event_emitter_spec.rb](./spec/unit/modules/event_emitter_spec.rb))_ * #emit event fan out @@ -4806,46 +4819,91 @@ _(see [spec/unit/realtime/safe_deferrable_spec.rb](./spec/unit/realtime/safe_def _(see [spec/unit/rest/channel_spec.rb](./spec/unit/rest/channel_spec.rb))_ * #initializer * as UTF_8 string - * [is permitted](./spec/unit/rest/channel_spec.rb#L24) - * [remains as UTF-8](./spec/unit/rest/channel_spec.rb#L28) + * [is permitted](./spec/unit/rest/channel_spec.rb#L25) + * [remains as UTF-8](./spec/unit/rest/channel_spec.rb#L29) * as frozen UTF_8 string - * [is permitted](./spec/unit/rest/channel_spec.rb#L37) - * [remains as UTF-8](./spec/unit/rest/channel_spec.rb#L41) + * [is permitted](./spec/unit/rest/channel_spec.rb#L38) + * [remains as UTF-8](./spec/unit/rest/channel_spec.rb#L42) * as SHIFT_JIS string - * [gets converted to UTF-8](./spec/unit/rest/channel_spec.rb#L49) - * [is compatible with original encoding](./spec/unit/rest/channel_spec.rb#L53) + * [gets converted to UTF-8](./spec/unit/rest/channel_spec.rb#L50) + * [is compatible with original encoding](./spec/unit/rest/channel_spec.rb#L54) * as ASCII_8BIT string - * [gets converted to UTF-8](./spec/unit/rest/channel_spec.rb#L61) - * [is compatible with original encoding](./spec/unit/rest/channel_spec.rb#L65) + * [gets converted to UTF-8](./spec/unit/rest/channel_spec.rb#L62) + * [is compatible with original encoding](./spec/unit/rest/channel_spec.rb#L66) * as Integer - * [raises an argument error](./spec/unit/rest/channel_spec.rb#L73) + * [raises an argument error](./spec/unit/rest/channel_spec.rb#L74) * as Nil - * [raises an argument error](./spec/unit/rest/channel_spec.rb#L81) + * [raises an argument error](./spec/unit/rest/channel_spec.rb#L82) * #publish name argument * as UTF_8 string - * [is permitted](./spec/unit/rest/channel_spec.rb#L93) + * [is permitted](./spec/unit/rest/channel_spec.rb#L94) * as frozen UTF_8 string - * [is permitted](./spec/unit/rest/channel_spec.rb#L102) + * [is permitted](./spec/unit/rest/channel_spec.rb#L103) * as SHIFT_JIS string - * [is permitted](./spec/unit/rest/channel_spec.rb#L110) + * [is permitted](./spec/unit/rest/channel_spec.rb#L111) * as ASCII_8BIT string - * [is permitted](./spec/unit/rest/channel_spec.rb#L118) + * [is permitted](./spec/unit/rest/channel_spec.rb#L119) * as Integer - * [raises an argument error](./spec/unit/rest/channel_spec.rb#L126) + * [raises an argument error](./spec/unit/rest/channel_spec.rb#L127) * max message size exceeded * when max_message_size is nil * and a message size is 65537 bytes - * [should raise Ably::Exceptions::MaxMessageSizeExceeded](./spec/unit/rest/channel_spec.rb#L134) + * [should raise Ably::Exceptions::MaxMessageSizeExceeded](./spec/unit/rest/channel_spec.rb#L135) * when max_message_size is 65536 bytes * and a message size is 65537 bytes - * [should raise Ably::Exceptions::MaxMessageSizeExceeded](./spec/unit/rest/channel_spec.rb#L144) + * [should raise Ably::Exceptions::MaxMessageSizeExceeded](./spec/unit/rest/channel_spec.rb#L145) * and a message size is 10 bytes - * [should send a message](./spec/unit/rest/channel_spec.rb#L150) + * [should send a message](./spec/unit/rest/channel_spec.rb#L151) * when max_message_size is 10 bytes * and a message size is 11 bytes - * [should raise Ably::Exceptions::MaxMessageSizeExceeded](./spec/unit/rest/channel_spec.rb#L160) + * [should raise Ably::Exceptions::MaxMessageSizeExceeded](./spec/unit/rest/channel_spec.rb#L161) * and a message size is 2 bytes - * [should send a message](./spec/unit/rest/channel_spec.rb#L166) + * [should send a message](./spec/unit/rest/channel_spec.rb#L167) + * #publish returns PublishResult (#RSL1n) + * with serials in response body + * [returns a PublishResult with serials](./spec/unit/rest/channel_spec.rb#L179) + * with empty response body (204) + * [returns a PublishResult with empty serials](./spec/unit/rest/channel_spec.rb#L189) + * with non-hash response body + * [returns a PublishResult with empty serials](./spec/unit/rest/channel_spec.rb#L199) + * #update_message (#RSL15) + * with a valid message containing serial + * [sends a PATCH request](./spec/unit/rest/channel_spec.rb#L224) + * [returns an UpdateDeleteResult](./spec/unit/rest/channel_spec.rb#L234) + * [sets action to MESSAGE_UPDATE](./spec/unit/rest/channel_spec.rb#L240) + * with an operation parameter + * [includes the operation as version in the payload](./spec/unit/rest/channel_spec.rb#L254) + * with a MessageOperation object + * [serializes the operation via as_json](./spec/unit/rest/channel_spec.rb#L268) + * without serial (#RSL15a) + * [raises an InvalidRequest exception](./spec/unit/rest/channel_spec.rb#L282) + * with a Hash message + * [converts to Message and validates serial](./spec/unit/rest/channel_spec.rb#L288) + * [works when serial is present](./spec/unit/rest/channel_spec.rb#L292) + * does not mutate the original message (#RSL15c) + * [the original message is unchanged](./spec/unit/rest/channel_spec.rb#L301) + * with query params (#RSL15f) + * [passes params as qs_params](./spec/unit/rest/channel_spec.rb#L312) + * #delete_message (#RSL15) + * with a valid message containing serial + * [sends a PATCH request with action MESSAGE_DELETE](./spec/unit/rest/channel_spec.rb#L341) + * [returns an UpdateDeleteResult](./spec/unit/rest/channel_spec.rb#L351) + * with an operation parameter + * [includes the operation as version in the payload](./spec/unit/rest/channel_spec.rb#L362) + * without serial + * [raises an InvalidRequest exception](./spec/unit/rest/channel_spec.rb#L375) + * does not mutate the original message + * [the original message is unchanged](./spec/unit/rest/channel_spec.rb#L383) + * #append_message (#RSL15) + * with a valid message containing serial + * [sends a PATCH request with action MESSAGE_APPEND](./spec/unit/rest/channel_spec.rb#L409) + * [returns an UpdateDeleteResult](./spec/unit/rest/channel_spec.rb#L419) + * with an operation parameter + * [includes the operation as version in the payload](./spec/unit/rest/channel_spec.rb#L430) + * without serial + * [raises an InvalidRequest exception](./spec/unit/rest/channel_spec.rb#L443) + * does not mutate the original message + * [the original message is unchanged](./spec/unit/rest/channel_spec.rb#L451) ### Ably::Rest::Channels _(see [spec/unit/rest/channels_spec.rb](./spec/unit/rest/channels_spec.rb))_ @@ -5075,6 +5133,6 @@ _(see [spec/unit/util/pub_sub_spec.rb](./spec/unit/util/pub_sub_spec.rb))_ ## Test summary - * Passing tests: 2495 + * Passing tests: 2511 * Pending tests: 5 * Failing tests: 0 diff --git a/lib/ably/version.rb b/lib/ably/version.rb index 08aaddc87..965b1e24b 100644 --- a/lib/ably/version.rb +++ b/lib/ably/version.rb @@ -1,5 +1,5 @@ module Ably - VERSION = '1.2.8' + VERSION = '1.3.0' # The level of compatibility with the Ably service that this SDK supports. # Also referred to as the 'wire protocol version'. # spec : CSV2 From 411d708d3657964abc659c0453ec9fb58b2afeb9 Mon Sep 17 00:00:00 2001 From: matt423 Date: Mon, 2 Mar 2026 14:27:06 +0000 Subject: [PATCH 7/8] Fix agent version regex to support multi-digit version segments The regex \d\.\d\.\d only matches single-digit version components but Ruby 3.2.10+ has double-digit patch versions, causing CI failures. --- spec/acceptance/realtime/connection_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/acceptance/realtime/connection_spec.rb b/spec/acceptance/realtime/connection_spec.rb index ca042f296..f52d4a91b 100644 --- a/spec/acceptance/realtime/connection_spec.rb +++ b/spec/acceptance/realtime/connection_spec.rb @@ -2005,7 +2005,7 @@ def self.available_states it 'sends the lib version param agent (#RCS7d)' do expect(EventMachine).to receive(:connect) do |host, port, transport, object, url| uri = URI.parse(url) - expect(CGI::parse(uri.query)['agent'][0]).to match(/^ably-ruby\/\d\.\d\.\d ruby\/\d\.\d\.\d$/) + expect(CGI::parse(uri.query)['agent'][0]).to match(/^ably-ruby\/\d+\.\d+\.\d+ ruby\/\d+\.\d+\.\d+$/) stop_reactor end client From 78f62a69599842a84302a01256ac9d6b77c35070 Mon Sep 17 00:00:00 2001 From: matt423 Date: Mon, 2 Mar 2026 15:26:21 +0000 Subject: [PATCH 8/8] Fix EventMachine test pollution causing cascade failures - Add after(:example) hook to clear stale realtime_clients and stop EM reactor between tests, preventing leaked state from crashed or timed-out tests poisoning subsequent EM-dependent tests - Switch paginated result specs from manual include + run_reactor to :event_machine metadata tag so they get proper before/around/after lifecycle hooks --- spec/support/event_machine_helper.rb | 35 +++++++++++++++++++ .../unit/models/http_paginated_result_spec.rb | 30 ++++++---------- spec/unit/models/paginated_result_spec.rb | 30 ++++++---------- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/spec/support/event_machine_helper.rb b/spec/support/event_machine_helper.rb index 0fa05e468..c1cd6f640 100644 --- a/spec/support/event_machine_helper.rb +++ b/spec/support/event_machine_helper.rb @@ -2,6 +2,26 @@ require 'rspec' require 'timeout' +# Prevent EM signal_loopbreak race condition from corrupting the entire +# process. When EM.defer is used and the reactor stops while a threadpool +# worker is mid-flight, the worker calls signal_loopbreak on a stopped +# reactor, raising RuntimeError. This poisons EM for the rest of the +# process, cascading failures to every subsequent test that touches EM. +# Swallowing the error is safe — signal_loopbreak is just a notification +# that there's work to process, and if EM isn't running there's nothing +# to notify. +module EventMachine + class << self + alias_method :original_signal_loopbreak, :signal_loopbreak + + def signal_loopbreak + original_signal_loopbreak + rescue RuntimeError + # EM not initialized — reactor was stopped between the check and the call + end + end +end + module RSpec module EventMachine extend self @@ -139,6 +159,21 @@ def wait_until(condition_block, &block) # Ensure EventMachine shutdown hooks are deregistered for every test EventMachine.instance_variable_set '@tails', [] end + + # Catch-all cleanup for ANY test that used EventMachine, whether via + # the :event_machine tag or by calling run_reactor directly. Without this, + # a crashed/timed-out reactor and stale client references leak into + # subsequent tests causing cascade failures. + config.after(:example) do + RSpec::EventMachine.realtime_clients.clear + begin + EventMachine.stop if EventMachine.reactor_running? + rescue RuntimeError + # EM can be in a corrupted state (e.g. signal_loopbreak failure) + # where reactor_running? returns true but stop raises. Swallow + # the error to prevent cascading failures across subsequent tests. + end + end end module RSpec diff --git a/spec/unit/models/http_paginated_result_spec.rb b/spec/unit/models/http_paginated_result_spec.rb index 1c4ac1990..6b9cc53cb 100644 --- a/spec/unit/models/http_paginated_result_spec.rb +++ b/spec/unit/models/http_paginated_result_spec.rb @@ -128,40 +128,32 @@ end if defined?(Ably::Realtime) - context 'with option async_blocking_operations: true' do - include RSpec::EventMachine - + context 'with option async_blocking_operations: true', :event_machine do subject do paginated_result_class.new(http_response, full_url, paged_client, async_blocking_operations: true) end context '#next' do it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do - run_reactor do - expect(subject.next).to be_a(Ably::Util::SafeDeferrable) - stop_reactor - end + expect(subject.next).to be_a(Ably::Util::SafeDeferrable) + stop_reactor end it 'allows a success callback block to be added' do - run_reactor do - subject.next do |paginated_result| - expect(paginated_result).to be_a(Ably::Models::HttpPaginatedResponse) - stop_reactor - end + subject.next do |paginated_result| + expect(paginated_result).to be_a(Ably::Models::HttpPaginatedResponse) + stop_reactor end end end context '#first' do it 'calls the errback callback when first page headers are missing' do - run_reactor do - subject.next do |paginated_result| - deferrable = subject.first - deferrable.errback do |error| - expect(error).to be_a(Ably::Exceptions::PageMissing) - stop_reactor - end + subject.next do |paginated_result| + deferrable = subject.first + deferrable.errback do |error| + expect(error).to be_a(Ably::Exceptions::PageMissing) + stop_reactor end end end diff --git a/spec/unit/models/paginated_result_spec.rb b/spec/unit/models/paginated_result_spec.rb index afd84c393..1775aa82b 100644 --- a/spec/unit/models/paginated_result_spec.rb +++ b/spec/unit/models/paginated_result_spec.rb @@ -126,40 +126,32 @@ end if defined?(Ably::Realtime) - context 'with option async_blocking_operations: true' do - include RSpec::EventMachine - + context 'with option async_blocking_operations: true', :event_machine do subject do paginated_result_class.new(http_response, full_url, paged_client, async_blocking_operations: true) end context '#next' do it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do - run_reactor do - expect(subject.next).to be_a(Ably::Util::SafeDeferrable) - stop_reactor - end + expect(subject.next).to be_a(Ably::Util::SafeDeferrable) + stop_reactor end it 'allows a success callback block to be added' do - run_reactor do - subject.next do |paginated_result| - expect(paginated_result).to be_a(Ably::Models::PaginatedResult) - stop_reactor - end + subject.next do |paginated_result| + expect(paginated_result).to be_a(Ably::Models::PaginatedResult) + stop_reactor end end end context '#first' do it 'calls the errback callback when first page headers are missing' do - run_reactor do - subject.next do |paginated_result| - deferrable = subject.first - deferrable.errback do |error| - expect(error).to be_a(Ably::Exceptions::PageMissing) - stop_reactor - end + subject.next do |paginated_result| + deferrable = subject.first + deferrable.errback do |error| + expect(error).to be_a(Ably::Exceptions::PageMissing) + stop_reactor end end end