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/models/message.rb b/lib/ably/models/message.rb index 5c5817b06..4c23c8870 100644 --- a/lib/ably/models/message.rb +++ b/lib/ably/models/message.rb @@ -26,6 +26,20 @@ 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 + :message_delete, # 2 + :meta, # 3 + :message_summary, # 4 + :message_append, # 5 + ) # Statically register a default set of encoders for this class Ably::Models::MessageEncoders.register_default_encoders self @@ -129,10 +143,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 +279,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/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/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/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..8273a435d 100644 --- a/lib/ably/realtime/channel.rb +++ b/lib/ably/realtime/channel.rb @@ -213,6 +213,56 @@ 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) + send_message_action(message, Ably::Models::Message::ACTION.MessageUpdate, operation, params, &success_block) + 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 + + # 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. @@ -405,6 +455,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/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 435d70660..c19a5ff81 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,11 +194,43 @@ 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, + 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) + 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) diff --git a/lib/ably/rest/channel.rb b/lib/ably/rest/channel.rb index a8bcfb83a..19d5827b5 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,54 @@ 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. + # + # @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 = {}) + send_message_action(message, Ably::Models::Message::ACTION.MessageUpdate, operation, params) + end + + # 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 + + # 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} @@ -178,6 +225,47 @@ 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) + 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/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..965b1e24b 100644 --- a/lib/ably/version.rb +++ b/lib/ably/version.rb @@ -1,7 +1,7 @@ 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 - PROTOCOL_VERSION = '2' + PROTOCOL_VERSION = '5' 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/connection_spec.rb b/spec/acceptance/realtime/connection_spec.rb index 36ca720fd..f52d4a91b 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 @@ -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 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/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/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/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..eb21f78f6 100644 --- a/spec/unit/models/message_spec.rb +++ b/spec/unit/models/message_spec.rb @@ -644,4 +644,169 @@ 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 + + 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 + + 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/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 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/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/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 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..d873a29b5 100644 --- a/spec/unit/realtime/incoming_message_dispatcher_spec.rb +++ b/spec/unit/realtime/incoming_message_dispatcher_spec.rb @@ -33,4 +33,206 @@ 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 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') + 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..11ef513e3 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,10 +165,295 @@ 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' }) } + 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' 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 + + 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 + + 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