From 2e4c0b8196b6cb9857278097ca070126f2933885 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 1 Sep 2026 16:18:53 +0100 Subject: [PATCH 01/13] Restructure into ably-pubsub-core and ably-pubsub-server gems Implements the PubSub package split (PDR-091b) for the Ruby SDK: - Move the implementation into core/ as the ably-pubsub-core gem, an internal package that only Ably packages depend on. The ably-common submodule moves with it. - Add server/ as the ably-pubsub-server gem, whose factory functions are the only recommended entry points: Ably::PubSub::Server.create_http_client and .create_realtime_client. Ruby is a server-only SDK under PDR-091b2, so there is no device gem. - The factories stamp the side-declaring agent entry ably-pubsub-server/ via a new additive :agents client option; the caller's own agent entries are preserved and the side entry wins a collision on its own identifier. The -server suffix is what MAU classification reads, so specs assert it loudly. - Core and server are versioned in lockstep at 2.0.0, with the server gem pinning the core dependency to the exact version. - The old ably gemspec is removed; the ably gem is maintained from a maintenance branch per the 1-year support window, not from here. Co-Authored-By: Claude Fable 5 --- .gitmodules | 2 +- .yardopts | 2 + Gemfile | 7 +- Rakefile | 6 +- core/README.md | 14 +++ ably.gemspec => core/ably-pubsub-core.gemspec | 18 ++-- {lib => core/lib}/ably.rb | 0 {lib => core/lib}/ably/agent.rb | 0 {lib => core/lib}/ably/auth.rb | 0 {lib => core/lib}/ably/exceptions.rb | 0 {lib => core/lib}/ably/logger.rb | 0 {lib => core/lib}/ably/models/auth_details.rb | 0 .../lib}/ably/models/channel_details.rb | 0 .../lib}/ably/models/channel_metrics.rb | 0 .../lib}/ably/models/channel_occupancy.rb | 0 .../lib}/ably/models/channel_options.rb | 0 .../lib}/ably/models/channel_state_change.rb | 0 .../lib}/ably/models/channel_status.rb | 0 .../lib}/ably/models/cipher_params.rb | 0 .../lib}/ably/models/connection_details.rb | 0 .../ably/models/connection_state_change.rb | 0 {lib => core/lib}/ably/models/delta_extras.rb | 0 .../lib}/ably/models/device_details.rb | 0 .../lib}/ably/models/device_push_details.rb | 0 {lib => core/lib}/ably/models/error_info.rb | 0 .../ably/models/http_paginated_response.rb | 0 .../ably/models/idiomatic_ruby_wrapper.rb | 0 {lib => core/lib}/ably/models/message.rb | 0 .../lib}/ably/models/message_encoders/base.rb | 0 .../ably/models/message_encoders/base64.rb | 0 .../ably/models/message_encoders/cipher.rb | 0 .../lib}/ably/models/message_encoders/json.rb | 0 .../lib}/ably/models/message_encoders/utf8.rb | 0 {lib => core/lib}/ably/models/nil_logger.rb | 0 .../lib}/ably/models/paginated_result.rb | 0 .../lib}/ably/models/presence_message.rb | 0 .../lib}/ably/models/protocol_message.rb | 0 .../ably/models/push_channel_subscription.rb | 0 {lib => core/lib}/ably/models/stats.rb | 0 {lib => core/lib}/ably/models/stats_types.rb | 0 .../lib}/ably/models/token_details.rb | 0 .../lib}/ably/models/token_request.rb | 0 {lib => core/lib}/ably/modules/ably.rb | 0 .../lib}/ably/modules/async_wrapper.rb | 0 .../lib}/ably/modules/channels_collection.rb | 0 {lib => core/lib}/ably/modules/conversions.rb | 0 {lib => core/lib}/ably/modules/encodeable.rb | 0 {lib => core/lib}/ably/modules/enum.rb | 0 .../lib}/ably/modules/event_emitter.rb | 0 .../ably/modules/event_machine_helpers.rb | 0 .../lib}/ably/modules/exception_codes.rb | 0 .../lib}/ably/modules/http_helpers.rb | 0 .../lib}/ably/modules/message_emitter.rb | 0 .../lib}/ably/modules/message_pack.rb | 0 .../lib}/ably/modules/model_common.rb | 0 .../lib}/ably/modules/safe_deferrable.rb | 0 {lib => core/lib}/ably/modules/safe_yield.rb | 0 .../lib}/ably/modules/state_emitter.rb | 0 .../lib}/ably/modules/state_machine.rb | 0 .../ably/modules/statesman_monkey_patch.rb | 0 .../lib}/ably/modules/uses_state_machine.rb | 0 {lib => core/lib}/ably/realtime.rb | 0 {lib => core/lib}/ably/realtime/auth.rb | 0 {lib => core/lib}/ably/realtime/channel.rb | 0 .../ably/realtime/channel/channel_manager.rb | 0 .../realtime/channel/channel_properties.rb | 0 .../realtime/channel/channel_state_machine.rb | 0 .../lib}/ably/realtime/channel/publisher.rb | 0 .../ably/realtime/channel/push_channel.rb | 0 {lib => core/lib}/ably/realtime/channels.rb | 0 {lib => core/lib}/ably/realtime/client.rb | 0 .../client/incoming_message_dispatcher.rb | 0 .../client/outgoing_message_dispatcher.rb | 0 {lib => core/lib}/ably/realtime/connection.rb | 0 .../realtime/connection/connection_manager.rb | 0 .../connection/connection_state_machine.rb | 0 .../connection/websocket_transport.rb | 0 .../lib}/ably/realtime/models/nil_channel.rb | 0 {lib => core/lib}/ably/realtime/presence.rb | 0 .../ably/realtime/presence/members_map.rb | 0 .../realtime/presence/presence_manager.rb | 0 .../presence/presence_state_machine.rb | 0 {lib => core/lib}/ably/realtime/push.rb | 0 {lib => core/lib}/ably/realtime/push/admin.rb | 0 .../realtime/push/channel_subscriptions.rb | 0 .../realtime/push/device_registrations.rb | 0 .../ably/realtime/recovery_key_context.rb | 0 {lib => core/lib}/ably/rest.rb | 0 {lib => core/lib}/ably/rest/channel.rb | 0 .../lib}/ably/rest/channel/push_channel.rb | 0 {lib => core/lib}/ably/rest/channels.rb | 0 {lib => core/lib}/ably/rest/client.rb | 7 ++ .../lib}/ably/rest/middleware/encoder.rb | 0 .../lib}/ably/rest/middleware/exceptions.rb | 0 .../rest/middleware/external_exceptions.rb | 0 .../fail_if_unsupported_mime_type.rb | 0 .../lib}/ably/rest/middleware/logger.rb | 0 .../lib}/ably/rest/middleware/parse_json.rb | 0 .../rest/middleware/parse_message_pack.rb | 0 {lib => core/lib}/ably/rest/presence.rb | 0 {lib => core/lib}/ably/rest/push.rb | 0 {lib => core/lib}/ably/rest/push/admin.rb | 0 .../ably/rest/push/channel_subscriptions.rb | 0 .../ably/rest/push/device_registrations.rb | 0 .../lib}/ably/util/ably_extensions.rb | 0 {lib => core/lib}/ably/util/crypto.rb | 0 {lib => core/lib}/ably/util/pub_sub.rb | 0 .../lib}/ably/util/safe_deferrable.rb | 0 {lib => core/lib}/ably/version.rb | 2 +- {lib => core/lib}/submodules/ably-common | 0 server/README.md | 33 +++++++ server/ably-pubsub-server.gemspec | 22 +++++ server/lib/ably/pubsub/server.rb | 70 +++++++++++++ server/lib/ably/pubsub/server/version.rb | 9 ++ spec/acceptance/realtime/message_spec.rb | 4 +- spec/acceptance/rest/message_spec.rb | 2 +- spec/support/test_app.rb | 2 +- spec/unit/pubsub/server_spec.rb | 97 +++++++++++++++++++ spec/unit/util/crypto_spec.rb | 2 +- 119 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 .yardopts create mode 100644 core/README.md rename ably.gemspec => core/ably-pubsub-core.gemspec (76%) rename {lib => core/lib}/ably.rb (100%) rename {lib => core/lib}/ably/agent.rb (100%) rename {lib => core/lib}/ably/auth.rb (100%) rename {lib => core/lib}/ably/exceptions.rb (100%) rename {lib => core/lib}/ably/logger.rb (100%) rename {lib => core/lib}/ably/models/auth_details.rb (100%) rename {lib => core/lib}/ably/models/channel_details.rb (100%) rename {lib => core/lib}/ably/models/channel_metrics.rb (100%) rename {lib => core/lib}/ably/models/channel_occupancy.rb (100%) rename {lib => core/lib}/ably/models/channel_options.rb (100%) rename {lib => core/lib}/ably/models/channel_state_change.rb (100%) rename {lib => core/lib}/ably/models/channel_status.rb (100%) rename {lib => core/lib}/ably/models/cipher_params.rb (100%) rename {lib => core/lib}/ably/models/connection_details.rb (100%) rename {lib => core/lib}/ably/models/connection_state_change.rb (100%) rename {lib => core/lib}/ably/models/delta_extras.rb (100%) rename {lib => core/lib}/ably/models/device_details.rb (100%) rename {lib => core/lib}/ably/models/device_push_details.rb (100%) rename {lib => core/lib}/ably/models/error_info.rb (100%) rename {lib => core/lib}/ably/models/http_paginated_response.rb (100%) rename {lib => core/lib}/ably/models/idiomatic_ruby_wrapper.rb (100%) rename {lib => core/lib}/ably/models/message.rb (100%) rename {lib => core/lib}/ably/models/message_encoders/base.rb (100%) rename {lib => core/lib}/ably/models/message_encoders/base64.rb (100%) rename {lib => core/lib}/ably/models/message_encoders/cipher.rb (100%) rename {lib => core/lib}/ably/models/message_encoders/json.rb (100%) rename {lib => core/lib}/ably/models/message_encoders/utf8.rb (100%) rename {lib => core/lib}/ably/models/nil_logger.rb (100%) rename {lib => core/lib}/ably/models/paginated_result.rb (100%) rename {lib => core/lib}/ably/models/presence_message.rb (100%) rename {lib => core/lib}/ably/models/protocol_message.rb (100%) rename {lib => core/lib}/ably/models/push_channel_subscription.rb (100%) rename {lib => core/lib}/ably/models/stats.rb (100%) rename {lib => core/lib}/ably/models/stats_types.rb (100%) rename {lib => core/lib}/ably/models/token_details.rb (100%) rename {lib => core/lib}/ably/models/token_request.rb (100%) rename {lib => core/lib}/ably/modules/ably.rb (100%) rename {lib => core/lib}/ably/modules/async_wrapper.rb (100%) rename {lib => core/lib}/ably/modules/channels_collection.rb (100%) rename {lib => core/lib}/ably/modules/conversions.rb (100%) rename {lib => core/lib}/ably/modules/encodeable.rb (100%) rename {lib => core/lib}/ably/modules/enum.rb (100%) rename {lib => core/lib}/ably/modules/event_emitter.rb (100%) rename {lib => core/lib}/ably/modules/event_machine_helpers.rb (100%) rename {lib => core/lib}/ably/modules/exception_codes.rb (100%) rename {lib => core/lib}/ably/modules/http_helpers.rb (100%) rename {lib => core/lib}/ably/modules/message_emitter.rb (100%) rename {lib => core/lib}/ably/modules/message_pack.rb (100%) rename {lib => core/lib}/ably/modules/model_common.rb (100%) rename {lib => core/lib}/ably/modules/safe_deferrable.rb (100%) rename {lib => core/lib}/ably/modules/safe_yield.rb (100%) rename {lib => core/lib}/ably/modules/state_emitter.rb (100%) rename {lib => core/lib}/ably/modules/state_machine.rb (100%) rename {lib => core/lib}/ably/modules/statesman_monkey_patch.rb (100%) rename {lib => core/lib}/ably/modules/uses_state_machine.rb (100%) rename {lib => core/lib}/ably/realtime.rb (100%) rename {lib => core/lib}/ably/realtime/auth.rb (100%) rename {lib => core/lib}/ably/realtime/channel.rb (100%) rename {lib => core/lib}/ably/realtime/channel/channel_manager.rb (100%) rename {lib => core/lib}/ably/realtime/channel/channel_properties.rb (100%) rename {lib => core/lib}/ably/realtime/channel/channel_state_machine.rb (100%) rename {lib => core/lib}/ably/realtime/channel/publisher.rb (100%) rename {lib => core/lib}/ably/realtime/channel/push_channel.rb (100%) rename {lib => core/lib}/ably/realtime/channels.rb (100%) rename {lib => core/lib}/ably/realtime/client.rb (100%) rename {lib => core/lib}/ably/realtime/client/incoming_message_dispatcher.rb (100%) rename {lib => core/lib}/ably/realtime/client/outgoing_message_dispatcher.rb (100%) rename {lib => core/lib}/ably/realtime/connection.rb (100%) rename {lib => core/lib}/ably/realtime/connection/connection_manager.rb (100%) rename {lib => core/lib}/ably/realtime/connection/connection_state_machine.rb (100%) rename {lib => core/lib}/ably/realtime/connection/websocket_transport.rb (100%) rename {lib => core/lib}/ably/realtime/models/nil_channel.rb (100%) rename {lib => core/lib}/ably/realtime/presence.rb (100%) rename {lib => core/lib}/ably/realtime/presence/members_map.rb (100%) rename {lib => core/lib}/ably/realtime/presence/presence_manager.rb (100%) rename {lib => core/lib}/ably/realtime/presence/presence_state_machine.rb (100%) rename {lib => core/lib}/ably/realtime/push.rb (100%) rename {lib => core/lib}/ably/realtime/push/admin.rb (100%) rename {lib => core/lib}/ably/realtime/push/channel_subscriptions.rb (100%) rename {lib => core/lib}/ably/realtime/push/device_registrations.rb (100%) rename {lib => core/lib}/ably/realtime/recovery_key_context.rb (100%) rename {lib => core/lib}/ably/rest.rb (100%) rename {lib => core/lib}/ably/rest/channel.rb (100%) rename {lib => core/lib}/ably/rest/channel/push_channel.rb (100%) rename {lib => core/lib}/ably/rest/channels.rb (100%) rename {lib => core/lib}/ably/rest/client.rb (98%) rename {lib => core/lib}/ably/rest/middleware/encoder.rb (100%) rename {lib => core/lib}/ably/rest/middleware/exceptions.rb (100%) rename {lib => core/lib}/ably/rest/middleware/external_exceptions.rb (100%) rename {lib => core/lib}/ably/rest/middleware/fail_if_unsupported_mime_type.rb (100%) rename {lib => core/lib}/ably/rest/middleware/logger.rb (100%) rename {lib => core/lib}/ably/rest/middleware/parse_json.rb (100%) rename {lib => core/lib}/ably/rest/middleware/parse_message_pack.rb (100%) rename {lib => core/lib}/ably/rest/presence.rb (100%) rename {lib => core/lib}/ably/rest/push.rb (100%) rename {lib => core/lib}/ably/rest/push/admin.rb (100%) rename {lib => core/lib}/ably/rest/push/channel_subscriptions.rb (100%) rename {lib => core/lib}/ably/rest/push/device_registrations.rb (100%) rename {lib => core/lib}/ably/util/ably_extensions.rb (100%) rename {lib => core/lib}/ably/util/crypto.rb (100%) rename {lib => core/lib}/ably/util/pub_sub.rb (100%) rename {lib => core/lib}/ably/util/safe_deferrable.rb (100%) rename {lib => core/lib}/ably/version.rb (90%) rename {lib => core/lib}/submodules/ably-common (100%) create mode 100644 server/README.md create mode 100644 server/ably-pubsub-server.gemspec create mode 100644 server/lib/ably/pubsub/server.rb create mode 100644 server/lib/ably/pubsub/server/version.rb create mode 100644 spec/unit/pubsub/server_spec.rb diff --git a/.gitmodules b/.gitmodules index 035940d12..548c849d2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "lib/submodules/ably-common"] - path = lib/submodules/ably-common + path = core/lib/submodules/ably-common url = https://github.com/ably/ably-common.git diff --git a/.yardopts b/.yardopts new file mode 100644 index 000000000..065c0fb12 --- /dev/null +++ b/.yardopts @@ -0,0 +1,2 @@ +core/lib/**/*.rb +server/lib/**/*.rb diff --git a/Gemfile b/Gemfile index 5d0856f56..4f54ce284 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,7 @@ source 'https://rubygems.org' -# Specify your gem's dependencies in ably.gemspec -gemspec +# This repo hosts two gems, released in lockstep at the same version (PDR-091b): +# - core/ ably-pubsub-core: the shared implementation (internal package) +# - server/ ably-pubsub-server: the public server-side package (factory entry points) +gemspec path: 'core' +gemspec path: 'server' diff --git a/Rakefile b/Rakefile index af2d779a8..99569d693 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ require 'rubygems' require 'bundler/setup' -require 'bundler/gem_tasks' + require 'json' require 'yard' @@ -32,8 +32,8 @@ begin desc 'Generate error code constants from ably-common: https://github.com/ably/ably-common/issues/32' task :generate_error_codes do - errors_json_path = File.join(File.dirname(__FILE__), 'lib/submodules/ably-common/protocol/errors.json') - module_path = File.join(File.dirname(__FILE__), 'lib/ably/modules/exception_codes.rb') + errors_json_path = File.join(File.dirname(__FILE__), 'core/lib/submodules/ably-common/protocol/errors.json') + module_path = File.join(File.dirname(__FILE__), 'core/lib/ably/modules/exception_codes.rb') max_length = 0 errors = JSON.parse(File.read(errors_json_path)).each_with_object({}) do |(key, val), hash| diff --git a/core/README.md b/core/README.md new file mode 100644 index 000000000..382d9e8f8 --- /dev/null +++ b/core/README.md @@ -0,0 +1,14 @@ +# ably-pubsub-core + +Internal implementation package for Ably's own Pub/Sub Ruby packages. + +**This gem is not intended for direct external use.** It is published only so that +Ably's public packages can depend on it. Use [`ably-pubsub-server`](../server) instead, +which exposes the supported entry points: + +```ruby +client = Ably::PubSub::Server.create_http_client(key) # stateless HTTP client +client = Ably::PubSub::Server.create_realtime_client(key) # stateful realtime client +``` + +`ably-pubsub-core` and `ably-pubsub-server` are released in lockstep at the same version. diff --git a/ably.gemspec b/core/ably-pubsub-core.gemspec similarity index 76% rename from ably.gemspec rename to core/ably-pubsub-core.gemspec index 765bdb702..b0d301101 100644 --- a/ably.gemspec +++ b/core/ably-pubsub-core.gemspec @@ -4,18 +4,18 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'ably/version' Gem::Specification.new do |spec| - spec.name = 'ably' + spec.name = 'ably-pubsub-core' spec.version = Ably::VERSION - spec.authors = ['Lewis Marshall', "Matthew O'Riordan"] - spec.email = ['lewis@lmars.net', 'matt@ably.io'] - spec.description = %q{A Ruby client library for ably.io realtime messaging} - spec.summary = %q{A Ruby client library for ably.io realtime messaging implemented using EventMachine} - spec.homepage = 'http://github.com/ably/ably-ruby' + spec.authors = ['Ably'] + spec.email = ['support@ably.com'] + spec.description = %q{Internal implementation package for Ably's own Pub/Sub packages. Not intended for direct external use: depend on ably-pubsub-server instead.} + spec.summary = %q{Shared core implementation for Ably Pub/Sub Ruby SDKs (internal)} + spec.homepage = 'https://github.com/ably/ably-ruby' spec.license = 'Apache-2.0' - spec.files = `git ls-files`.split($/) - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.files = Dir.chdir(File.expand_path(__dir__)) do + `git ls-files -z lib`.split("\x0").reject { |f| f.start_with?('lib/submodules') } + end spec.require_paths = ['lib'] spec.add_runtime_dependency 'eventmachine', '~> 1.2.6' diff --git a/lib/ably.rb b/core/lib/ably.rb similarity index 100% rename from lib/ably.rb rename to core/lib/ably.rb diff --git a/lib/ably/agent.rb b/core/lib/ably/agent.rb similarity index 100% rename from lib/ably/agent.rb rename to core/lib/ably/agent.rb diff --git a/lib/ably/auth.rb b/core/lib/ably/auth.rb similarity index 100% rename from lib/ably/auth.rb rename to core/lib/ably/auth.rb diff --git a/lib/ably/exceptions.rb b/core/lib/ably/exceptions.rb similarity index 100% rename from lib/ably/exceptions.rb rename to core/lib/ably/exceptions.rb diff --git a/lib/ably/logger.rb b/core/lib/ably/logger.rb similarity index 100% rename from lib/ably/logger.rb rename to core/lib/ably/logger.rb diff --git a/lib/ably/models/auth_details.rb b/core/lib/ably/models/auth_details.rb similarity index 100% rename from lib/ably/models/auth_details.rb rename to core/lib/ably/models/auth_details.rb diff --git a/lib/ably/models/channel_details.rb b/core/lib/ably/models/channel_details.rb similarity index 100% rename from lib/ably/models/channel_details.rb rename to core/lib/ably/models/channel_details.rb diff --git a/lib/ably/models/channel_metrics.rb b/core/lib/ably/models/channel_metrics.rb similarity index 100% rename from lib/ably/models/channel_metrics.rb rename to core/lib/ably/models/channel_metrics.rb diff --git a/lib/ably/models/channel_occupancy.rb b/core/lib/ably/models/channel_occupancy.rb similarity index 100% rename from lib/ably/models/channel_occupancy.rb rename to core/lib/ably/models/channel_occupancy.rb diff --git a/lib/ably/models/channel_options.rb b/core/lib/ably/models/channel_options.rb similarity index 100% rename from lib/ably/models/channel_options.rb rename to core/lib/ably/models/channel_options.rb diff --git a/lib/ably/models/channel_state_change.rb b/core/lib/ably/models/channel_state_change.rb similarity index 100% rename from lib/ably/models/channel_state_change.rb rename to core/lib/ably/models/channel_state_change.rb diff --git a/lib/ably/models/channel_status.rb b/core/lib/ably/models/channel_status.rb similarity index 100% rename from lib/ably/models/channel_status.rb rename to core/lib/ably/models/channel_status.rb diff --git a/lib/ably/models/cipher_params.rb b/core/lib/ably/models/cipher_params.rb similarity index 100% rename from lib/ably/models/cipher_params.rb rename to core/lib/ably/models/cipher_params.rb diff --git a/lib/ably/models/connection_details.rb b/core/lib/ably/models/connection_details.rb similarity index 100% rename from lib/ably/models/connection_details.rb rename to core/lib/ably/models/connection_details.rb diff --git a/lib/ably/models/connection_state_change.rb b/core/lib/ably/models/connection_state_change.rb similarity index 100% rename from lib/ably/models/connection_state_change.rb rename to core/lib/ably/models/connection_state_change.rb diff --git a/lib/ably/models/delta_extras.rb b/core/lib/ably/models/delta_extras.rb similarity index 100% rename from lib/ably/models/delta_extras.rb rename to core/lib/ably/models/delta_extras.rb diff --git a/lib/ably/models/device_details.rb b/core/lib/ably/models/device_details.rb similarity index 100% rename from lib/ably/models/device_details.rb rename to core/lib/ably/models/device_details.rb diff --git a/lib/ably/models/device_push_details.rb b/core/lib/ably/models/device_push_details.rb similarity index 100% rename from lib/ably/models/device_push_details.rb rename to core/lib/ably/models/device_push_details.rb diff --git a/lib/ably/models/error_info.rb b/core/lib/ably/models/error_info.rb similarity index 100% rename from lib/ably/models/error_info.rb rename to core/lib/ably/models/error_info.rb diff --git a/lib/ably/models/http_paginated_response.rb b/core/lib/ably/models/http_paginated_response.rb similarity index 100% rename from lib/ably/models/http_paginated_response.rb rename to core/lib/ably/models/http_paginated_response.rb diff --git a/lib/ably/models/idiomatic_ruby_wrapper.rb b/core/lib/ably/models/idiomatic_ruby_wrapper.rb similarity index 100% rename from lib/ably/models/idiomatic_ruby_wrapper.rb rename to core/lib/ably/models/idiomatic_ruby_wrapper.rb diff --git a/lib/ably/models/message.rb b/core/lib/ably/models/message.rb similarity index 100% rename from lib/ably/models/message.rb rename to core/lib/ably/models/message.rb diff --git a/lib/ably/models/message_encoders/base.rb b/core/lib/ably/models/message_encoders/base.rb similarity index 100% rename from lib/ably/models/message_encoders/base.rb rename to core/lib/ably/models/message_encoders/base.rb diff --git a/lib/ably/models/message_encoders/base64.rb b/core/lib/ably/models/message_encoders/base64.rb similarity index 100% rename from lib/ably/models/message_encoders/base64.rb rename to core/lib/ably/models/message_encoders/base64.rb diff --git a/lib/ably/models/message_encoders/cipher.rb b/core/lib/ably/models/message_encoders/cipher.rb similarity index 100% rename from lib/ably/models/message_encoders/cipher.rb rename to core/lib/ably/models/message_encoders/cipher.rb diff --git a/lib/ably/models/message_encoders/json.rb b/core/lib/ably/models/message_encoders/json.rb similarity index 100% rename from lib/ably/models/message_encoders/json.rb rename to core/lib/ably/models/message_encoders/json.rb diff --git a/lib/ably/models/message_encoders/utf8.rb b/core/lib/ably/models/message_encoders/utf8.rb similarity index 100% rename from lib/ably/models/message_encoders/utf8.rb rename to core/lib/ably/models/message_encoders/utf8.rb diff --git a/lib/ably/models/nil_logger.rb b/core/lib/ably/models/nil_logger.rb similarity index 100% rename from lib/ably/models/nil_logger.rb rename to core/lib/ably/models/nil_logger.rb diff --git a/lib/ably/models/paginated_result.rb b/core/lib/ably/models/paginated_result.rb similarity index 100% rename from lib/ably/models/paginated_result.rb rename to core/lib/ably/models/paginated_result.rb diff --git a/lib/ably/models/presence_message.rb b/core/lib/ably/models/presence_message.rb similarity index 100% rename from lib/ably/models/presence_message.rb rename to core/lib/ably/models/presence_message.rb diff --git a/lib/ably/models/protocol_message.rb b/core/lib/ably/models/protocol_message.rb similarity index 100% rename from lib/ably/models/protocol_message.rb rename to core/lib/ably/models/protocol_message.rb diff --git a/lib/ably/models/push_channel_subscription.rb b/core/lib/ably/models/push_channel_subscription.rb similarity index 100% rename from lib/ably/models/push_channel_subscription.rb rename to core/lib/ably/models/push_channel_subscription.rb diff --git a/lib/ably/models/stats.rb b/core/lib/ably/models/stats.rb similarity index 100% rename from lib/ably/models/stats.rb rename to core/lib/ably/models/stats.rb diff --git a/lib/ably/models/stats_types.rb b/core/lib/ably/models/stats_types.rb similarity index 100% rename from lib/ably/models/stats_types.rb rename to core/lib/ably/models/stats_types.rb diff --git a/lib/ably/models/token_details.rb b/core/lib/ably/models/token_details.rb similarity index 100% rename from lib/ably/models/token_details.rb rename to core/lib/ably/models/token_details.rb diff --git a/lib/ably/models/token_request.rb b/core/lib/ably/models/token_request.rb similarity index 100% rename from lib/ably/models/token_request.rb rename to core/lib/ably/models/token_request.rb diff --git a/lib/ably/modules/ably.rb b/core/lib/ably/modules/ably.rb similarity index 100% rename from lib/ably/modules/ably.rb rename to core/lib/ably/modules/ably.rb diff --git a/lib/ably/modules/async_wrapper.rb b/core/lib/ably/modules/async_wrapper.rb similarity index 100% rename from lib/ably/modules/async_wrapper.rb rename to core/lib/ably/modules/async_wrapper.rb diff --git a/lib/ably/modules/channels_collection.rb b/core/lib/ably/modules/channels_collection.rb similarity index 100% rename from lib/ably/modules/channels_collection.rb rename to core/lib/ably/modules/channels_collection.rb diff --git a/lib/ably/modules/conversions.rb b/core/lib/ably/modules/conversions.rb similarity index 100% rename from lib/ably/modules/conversions.rb rename to core/lib/ably/modules/conversions.rb diff --git a/lib/ably/modules/encodeable.rb b/core/lib/ably/modules/encodeable.rb similarity index 100% rename from lib/ably/modules/encodeable.rb rename to core/lib/ably/modules/encodeable.rb diff --git a/lib/ably/modules/enum.rb b/core/lib/ably/modules/enum.rb similarity index 100% rename from lib/ably/modules/enum.rb rename to core/lib/ably/modules/enum.rb diff --git a/lib/ably/modules/event_emitter.rb b/core/lib/ably/modules/event_emitter.rb similarity index 100% rename from lib/ably/modules/event_emitter.rb rename to core/lib/ably/modules/event_emitter.rb diff --git a/lib/ably/modules/event_machine_helpers.rb b/core/lib/ably/modules/event_machine_helpers.rb similarity index 100% rename from lib/ably/modules/event_machine_helpers.rb rename to core/lib/ably/modules/event_machine_helpers.rb diff --git a/lib/ably/modules/exception_codes.rb b/core/lib/ably/modules/exception_codes.rb similarity index 100% rename from lib/ably/modules/exception_codes.rb rename to core/lib/ably/modules/exception_codes.rb diff --git a/lib/ably/modules/http_helpers.rb b/core/lib/ably/modules/http_helpers.rb similarity index 100% rename from lib/ably/modules/http_helpers.rb rename to core/lib/ably/modules/http_helpers.rb diff --git a/lib/ably/modules/message_emitter.rb b/core/lib/ably/modules/message_emitter.rb similarity index 100% rename from lib/ably/modules/message_emitter.rb rename to core/lib/ably/modules/message_emitter.rb diff --git a/lib/ably/modules/message_pack.rb b/core/lib/ably/modules/message_pack.rb similarity index 100% rename from lib/ably/modules/message_pack.rb rename to core/lib/ably/modules/message_pack.rb diff --git a/lib/ably/modules/model_common.rb b/core/lib/ably/modules/model_common.rb similarity index 100% rename from lib/ably/modules/model_common.rb rename to core/lib/ably/modules/model_common.rb diff --git a/lib/ably/modules/safe_deferrable.rb b/core/lib/ably/modules/safe_deferrable.rb similarity index 100% rename from lib/ably/modules/safe_deferrable.rb rename to core/lib/ably/modules/safe_deferrable.rb diff --git a/lib/ably/modules/safe_yield.rb b/core/lib/ably/modules/safe_yield.rb similarity index 100% rename from lib/ably/modules/safe_yield.rb rename to core/lib/ably/modules/safe_yield.rb diff --git a/lib/ably/modules/state_emitter.rb b/core/lib/ably/modules/state_emitter.rb similarity index 100% rename from lib/ably/modules/state_emitter.rb rename to core/lib/ably/modules/state_emitter.rb diff --git a/lib/ably/modules/state_machine.rb b/core/lib/ably/modules/state_machine.rb similarity index 100% rename from lib/ably/modules/state_machine.rb rename to core/lib/ably/modules/state_machine.rb diff --git a/lib/ably/modules/statesman_monkey_patch.rb b/core/lib/ably/modules/statesman_monkey_patch.rb similarity index 100% rename from lib/ably/modules/statesman_monkey_patch.rb rename to core/lib/ably/modules/statesman_monkey_patch.rb diff --git a/lib/ably/modules/uses_state_machine.rb b/core/lib/ably/modules/uses_state_machine.rb similarity index 100% rename from lib/ably/modules/uses_state_machine.rb rename to core/lib/ably/modules/uses_state_machine.rb diff --git a/lib/ably/realtime.rb b/core/lib/ably/realtime.rb similarity index 100% rename from lib/ably/realtime.rb rename to core/lib/ably/realtime.rb diff --git a/lib/ably/realtime/auth.rb b/core/lib/ably/realtime/auth.rb similarity index 100% rename from lib/ably/realtime/auth.rb rename to core/lib/ably/realtime/auth.rb diff --git a/lib/ably/realtime/channel.rb b/core/lib/ably/realtime/channel.rb similarity index 100% rename from lib/ably/realtime/channel.rb rename to core/lib/ably/realtime/channel.rb diff --git a/lib/ably/realtime/channel/channel_manager.rb b/core/lib/ably/realtime/channel/channel_manager.rb similarity index 100% rename from lib/ably/realtime/channel/channel_manager.rb rename to core/lib/ably/realtime/channel/channel_manager.rb diff --git a/lib/ably/realtime/channel/channel_properties.rb b/core/lib/ably/realtime/channel/channel_properties.rb similarity index 100% rename from lib/ably/realtime/channel/channel_properties.rb rename to core/lib/ably/realtime/channel/channel_properties.rb diff --git a/lib/ably/realtime/channel/channel_state_machine.rb b/core/lib/ably/realtime/channel/channel_state_machine.rb similarity index 100% rename from lib/ably/realtime/channel/channel_state_machine.rb rename to core/lib/ably/realtime/channel/channel_state_machine.rb diff --git a/lib/ably/realtime/channel/publisher.rb b/core/lib/ably/realtime/channel/publisher.rb similarity index 100% rename from lib/ably/realtime/channel/publisher.rb rename to core/lib/ably/realtime/channel/publisher.rb diff --git a/lib/ably/realtime/channel/push_channel.rb b/core/lib/ably/realtime/channel/push_channel.rb similarity index 100% rename from lib/ably/realtime/channel/push_channel.rb rename to core/lib/ably/realtime/channel/push_channel.rb diff --git a/lib/ably/realtime/channels.rb b/core/lib/ably/realtime/channels.rb similarity index 100% rename from lib/ably/realtime/channels.rb rename to core/lib/ably/realtime/channels.rb diff --git a/lib/ably/realtime/client.rb b/core/lib/ably/realtime/client.rb similarity index 100% rename from lib/ably/realtime/client.rb rename to core/lib/ably/realtime/client.rb diff --git a/lib/ably/realtime/client/incoming_message_dispatcher.rb b/core/lib/ably/realtime/client/incoming_message_dispatcher.rb similarity index 100% rename from lib/ably/realtime/client/incoming_message_dispatcher.rb rename to core/lib/ably/realtime/client/incoming_message_dispatcher.rb diff --git a/lib/ably/realtime/client/outgoing_message_dispatcher.rb b/core/lib/ably/realtime/client/outgoing_message_dispatcher.rb similarity index 100% rename from lib/ably/realtime/client/outgoing_message_dispatcher.rb rename to core/lib/ably/realtime/client/outgoing_message_dispatcher.rb diff --git a/lib/ably/realtime/connection.rb b/core/lib/ably/realtime/connection.rb similarity index 100% rename from lib/ably/realtime/connection.rb rename to core/lib/ably/realtime/connection.rb diff --git a/lib/ably/realtime/connection/connection_manager.rb b/core/lib/ably/realtime/connection/connection_manager.rb similarity index 100% rename from lib/ably/realtime/connection/connection_manager.rb rename to core/lib/ably/realtime/connection/connection_manager.rb diff --git a/lib/ably/realtime/connection/connection_state_machine.rb b/core/lib/ably/realtime/connection/connection_state_machine.rb similarity index 100% rename from lib/ably/realtime/connection/connection_state_machine.rb rename to core/lib/ably/realtime/connection/connection_state_machine.rb diff --git a/lib/ably/realtime/connection/websocket_transport.rb b/core/lib/ably/realtime/connection/websocket_transport.rb similarity index 100% rename from lib/ably/realtime/connection/websocket_transport.rb rename to core/lib/ably/realtime/connection/websocket_transport.rb diff --git a/lib/ably/realtime/models/nil_channel.rb b/core/lib/ably/realtime/models/nil_channel.rb similarity index 100% rename from lib/ably/realtime/models/nil_channel.rb rename to core/lib/ably/realtime/models/nil_channel.rb diff --git a/lib/ably/realtime/presence.rb b/core/lib/ably/realtime/presence.rb similarity index 100% rename from lib/ably/realtime/presence.rb rename to core/lib/ably/realtime/presence.rb diff --git a/lib/ably/realtime/presence/members_map.rb b/core/lib/ably/realtime/presence/members_map.rb similarity index 100% rename from lib/ably/realtime/presence/members_map.rb rename to core/lib/ably/realtime/presence/members_map.rb diff --git a/lib/ably/realtime/presence/presence_manager.rb b/core/lib/ably/realtime/presence/presence_manager.rb similarity index 100% rename from lib/ably/realtime/presence/presence_manager.rb rename to core/lib/ably/realtime/presence/presence_manager.rb diff --git a/lib/ably/realtime/presence/presence_state_machine.rb b/core/lib/ably/realtime/presence/presence_state_machine.rb similarity index 100% rename from lib/ably/realtime/presence/presence_state_machine.rb rename to core/lib/ably/realtime/presence/presence_state_machine.rb diff --git a/lib/ably/realtime/push.rb b/core/lib/ably/realtime/push.rb similarity index 100% rename from lib/ably/realtime/push.rb rename to core/lib/ably/realtime/push.rb diff --git a/lib/ably/realtime/push/admin.rb b/core/lib/ably/realtime/push/admin.rb similarity index 100% rename from lib/ably/realtime/push/admin.rb rename to core/lib/ably/realtime/push/admin.rb diff --git a/lib/ably/realtime/push/channel_subscriptions.rb b/core/lib/ably/realtime/push/channel_subscriptions.rb similarity index 100% rename from lib/ably/realtime/push/channel_subscriptions.rb rename to core/lib/ably/realtime/push/channel_subscriptions.rb diff --git a/lib/ably/realtime/push/device_registrations.rb b/core/lib/ably/realtime/push/device_registrations.rb similarity index 100% rename from lib/ably/realtime/push/device_registrations.rb rename to core/lib/ably/realtime/push/device_registrations.rb diff --git a/lib/ably/realtime/recovery_key_context.rb b/core/lib/ably/realtime/recovery_key_context.rb similarity index 100% rename from lib/ably/realtime/recovery_key_context.rb rename to core/lib/ably/realtime/recovery_key_context.rb diff --git a/lib/ably/rest.rb b/core/lib/ably/rest.rb similarity index 100% rename from lib/ably/rest.rb rename to core/lib/ably/rest.rb diff --git a/lib/ably/rest/channel.rb b/core/lib/ably/rest/channel.rb similarity index 100% rename from lib/ably/rest/channel.rb rename to core/lib/ably/rest/channel.rb diff --git a/lib/ably/rest/channel/push_channel.rb b/core/lib/ably/rest/channel/push_channel.rb similarity index 100% rename from lib/ably/rest/channel/push_channel.rb rename to core/lib/ably/rest/channel/push_channel.rb diff --git a/lib/ably/rest/channels.rb b/core/lib/ably/rest/channels.rb similarity index 100% rename from lib/ably/rest/channels.rb rename to core/lib/ably/rest/channels.rb diff --git a/lib/ably/rest/client.rb b/core/lib/ably/rest/client.rb similarity index 98% rename from lib/ably/rest/client.rb rename to core/lib/ably/rest/client.rb index 3dbda2dfb..523f314ec 100644 --- a/lib/ably/rest/client.rb +++ b/core/lib/ably/rest/client.rb @@ -186,6 +186,13 @@ def initialize(options) end @agent = options.delete(:agent) || Ably::AGENT + # Additive agent entries (`identifier => version`), appended to the base agent + # string. This is how a package layered on this one (such as ably-pubsub-server) + # declares itself — including the side-declaring entry that MAU classification + # reads — without replacing the base identifiers the way :agent does. + options.delete(:agents).to_h.each do |identifier, version| + @agent = "#{@agent} #{version ? "#{identifier}/#{version}" : identifier}" + end @realtime_client = options.delete(:realtime_client) @tls = options.delete_with_default(:tls, true) @environment = options.delete(:environment) # nil is production diff --git a/lib/ably/rest/middleware/encoder.rb b/core/lib/ably/rest/middleware/encoder.rb similarity index 100% rename from lib/ably/rest/middleware/encoder.rb rename to core/lib/ably/rest/middleware/encoder.rb diff --git a/lib/ably/rest/middleware/exceptions.rb b/core/lib/ably/rest/middleware/exceptions.rb similarity index 100% rename from lib/ably/rest/middleware/exceptions.rb rename to core/lib/ably/rest/middleware/exceptions.rb diff --git a/lib/ably/rest/middleware/external_exceptions.rb b/core/lib/ably/rest/middleware/external_exceptions.rb similarity index 100% rename from lib/ably/rest/middleware/external_exceptions.rb rename to core/lib/ably/rest/middleware/external_exceptions.rb diff --git a/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb b/core/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb similarity index 100% rename from lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb rename to core/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb diff --git a/lib/ably/rest/middleware/logger.rb b/core/lib/ably/rest/middleware/logger.rb similarity index 100% rename from lib/ably/rest/middleware/logger.rb rename to core/lib/ably/rest/middleware/logger.rb diff --git a/lib/ably/rest/middleware/parse_json.rb b/core/lib/ably/rest/middleware/parse_json.rb similarity index 100% rename from lib/ably/rest/middleware/parse_json.rb rename to core/lib/ably/rest/middleware/parse_json.rb diff --git a/lib/ably/rest/middleware/parse_message_pack.rb b/core/lib/ably/rest/middleware/parse_message_pack.rb similarity index 100% rename from lib/ably/rest/middleware/parse_message_pack.rb rename to core/lib/ably/rest/middleware/parse_message_pack.rb diff --git a/lib/ably/rest/presence.rb b/core/lib/ably/rest/presence.rb similarity index 100% rename from lib/ably/rest/presence.rb rename to core/lib/ably/rest/presence.rb diff --git a/lib/ably/rest/push.rb b/core/lib/ably/rest/push.rb similarity index 100% rename from lib/ably/rest/push.rb rename to core/lib/ably/rest/push.rb diff --git a/lib/ably/rest/push/admin.rb b/core/lib/ably/rest/push/admin.rb similarity index 100% rename from lib/ably/rest/push/admin.rb rename to core/lib/ably/rest/push/admin.rb diff --git a/lib/ably/rest/push/channel_subscriptions.rb b/core/lib/ably/rest/push/channel_subscriptions.rb similarity index 100% rename from lib/ably/rest/push/channel_subscriptions.rb rename to core/lib/ably/rest/push/channel_subscriptions.rb diff --git a/lib/ably/rest/push/device_registrations.rb b/core/lib/ably/rest/push/device_registrations.rb similarity index 100% rename from lib/ably/rest/push/device_registrations.rb rename to core/lib/ably/rest/push/device_registrations.rb diff --git a/lib/ably/util/ably_extensions.rb b/core/lib/ably/util/ably_extensions.rb similarity index 100% rename from lib/ably/util/ably_extensions.rb rename to core/lib/ably/util/ably_extensions.rb diff --git a/lib/ably/util/crypto.rb b/core/lib/ably/util/crypto.rb similarity index 100% rename from lib/ably/util/crypto.rb rename to core/lib/ably/util/crypto.rb diff --git a/lib/ably/util/pub_sub.rb b/core/lib/ably/util/pub_sub.rb similarity index 100% rename from lib/ably/util/pub_sub.rb rename to core/lib/ably/util/pub_sub.rb diff --git a/lib/ably/util/safe_deferrable.rb b/core/lib/ably/util/safe_deferrable.rb similarity index 100% rename from lib/ably/util/safe_deferrable.rb rename to core/lib/ably/util/safe_deferrable.rb diff --git a/lib/ably/version.rb b/core/lib/ably/version.rb similarity index 90% rename from lib/ably/version.rb rename to core/lib/ably/version.rb index 64eef5be7..ed629fa87 100644 --- a/lib/ably/version.rb +++ b/core/lib/ably/version.rb @@ -1,5 +1,5 @@ module Ably - VERSION = '1.2.8' + VERSION = '2.0.0' # The level of compatibility with the Ably service that this SDK supports. # Also referred to as the 'wire protocol version'. # spec : CSV2 diff --git a/lib/submodules/ably-common b/core/lib/submodules/ably-common similarity index 100% rename from lib/submodules/ably-common rename to core/lib/submodules/ably-common diff --git a/server/README.md b/server/README.md new file mode 100644 index 000000000..da5eab247 --- /dev/null +++ b/server/README.md @@ -0,0 +1,33 @@ +# ably-pubsub-server + +Ably Pub/Sub client for servers: backend services and other trusted runtimes. + +Installing this package declares that its traffic originates from the server side — +[MAU classification](https://ably.com/pricing) is a side effect of the install decision. + +## Installation + +```ruby +gem 'ably-pubsub-server' +``` + +## Usage + +The factory functions are the only recommended entry points: + +```ruby +require 'ably/pubsub/server' + +# Stateless HTTP client: publish, history, presence reads, token issuance +http_client = Ably::PubSub::Server.create_http_client('your-api-key') +http_client.channels.get('example').publish('event', 'payload') + +# Stateful realtime client: a persistent, live connection (EventMachine-based) +realtime_client = Ably::PubSub::Server.create_realtime_client('your-api-key') +``` + +Both factories accept everything the underlying constructors accept: an options `Hash`, +an API key `String`, or a token `String`. + +This gem is built on `ably-pubsub-core`, an internal implementation package. Depend on +this gem, not on the core. diff --git a/server/ably-pubsub-server.gemspec b/server/ably-pubsub-server.gemspec new file mode 100644 index 000000000..57cfcbb54 --- /dev/null +++ b/server/ably-pubsub-server.gemspec @@ -0,0 +1,22 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'ably/pubsub/server/version' + +Gem::Specification.new do |spec| + spec.name = 'ably-pubsub-server' + spec.version = Ably::PubSub::Server::VERSION + spec.authors = ['Ably'] + spec.email = ['support@ably.com'] + spec.description = %q{Ably Pub/Sub client for servers: backend services and other trusted runtimes. Construct clients with Ably::PubSub::Server.create_http_client or Ably::PubSub::Server.create_realtime_client.} + spec.summary = %q{Ably Pub/Sub client for servers} + spec.homepage = 'https://github.com/ably/ably-ruby' + spec.license = 'Apache-2.0' + + spec.files = Dir.chdir(File.expand_path(__dir__)) { `git ls-files -z lib README.md`.split("\x0") } + spec.require_paths = ['lib'] + + # Exact pin: core and server are released in lockstep at the same version (PDR-091b), + # which also guarantees a consumer can never resolve two different core versions. + spec.add_runtime_dependency 'ably-pubsub-core', "= #{Ably::PubSub::Server::VERSION}" +end diff --git a/server/lib/ably/pubsub/server.rb b/server/lib/ably/pubsub/server.rb new file mode 100644 index 000000000..56b1ac346 --- /dev/null +++ b/server/lib/ably/pubsub/server.rb @@ -0,0 +1,70 @@ +require 'ably' +require 'ably/pubsub/server/version' + +module Ably + module PubSub + # The Ably Pub/Sub SDK for servers. The factory functions here are the only + # recommended entry points of the +ably-pubsub-server+ gem. + module Server + # The agent identifier declaring the server side. + # + # The `-server` suffix is load-bearing, not cosmetic. On API-key auth the realtime + # system grants the MAU server exemption by matching an agent entry ending in + # `-server`, and an identifier that is not yet in the ably-common registry is + # classified by that suffix alone. Renaming it without preserving the suffix + # silently reclassifies every client this package constructs. + SERVER_AGENT_IDENTIFIER = 'ably-pubsub-server' + + class << self + # Creates a stateless HTTP (REST) client declaring the server side. + # + # Accepts everything {Ably::Rest::Client#initialize} accepts: an options Hash, + # an API key String, or a token String. + # + # @return [Ably::Rest::Client] + def create_http_client(options) + Ably::Rest::Client.new(options_with_side_agent(options)) + end + + # Creates a stateful realtime client declaring the server side. + # + # Accepts everything {Ably::Realtime::Client#initialize} accepts: an options Hash, + # an API key String, or a token String. + # + # @return [Ably::Realtime::Client] + def create_realtime_client(options) + Ably::Realtime::Client.new(options_with_side_agent(options)) + end + + private + + # Returns a copy of the caller's options carrying the agent entry that declares + # this package's side. + # + # The caller's own +:agents+ entries are preserved, so an SDK layered on top of + # this package keeps its attribution. The side entry is merged last and so wins a + # collision on its own identifier: which side the package declares is the + # package's to state, not the caller's to redefine. + # + # A nil argument passes through unchanged so the caller gets the core + # constructor's own initialization error rather than a vaguer failure later. + def options_with_side_agent(options) + return options if options.nil? + + options = if options.kind_of?(String) + if options.match(Ably::Auth::API_KEY_REGEX) + { key: options } + else + { token: options } + end + else + options.clone + end + + agents = options[:agents].to_h.merge(SERVER_AGENT_IDENTIFIER => VERSION) + options.merge(agents: agents) + end + end + end + end +end diff --git a/server/lib/ably/pubsub/server/version.rb b/server/lib/ably/pubsub/server/version.rb new file mode 100644 index 000000000..13d294b22 --- /dev/null +++ b/server/lib/ably/pubsub/server/version.rb @@ -0,0 +1,9 @@ +module Ably + module PubSub + module Server + # Must match Ably::VERSION in ably-pubsub-core: core and server are released in + # lockstep at the same version (PDR-091b). The release workflow and a spec assert this. + VERSION = '2.0.0' + end + end +end diff --git a/spec/acceptance/realtime/message_spec.rb b/spec/acceptance/realtime/message_spec.rb index b218a8e13..655ae93cf 100644 --- a/spec/acceptance/realtime/message_spec.rb +++ b/spec/acceptance/realtime/message_spec.rb @@ -485,7 +485,7 @@ def publish_and_check_extras(extras) end end - resources_root = File.expand_path('../../../../lib/submodules/ably-common/test-resources', __FILE__) + resources_root = File.expand_path('../../../../core/lib/submodules/ably-common/test-resources', __FILE__) shared_examples 'add_tests_for_data' do |data| data['items'].each_with_index do |item, index| @@ -823,7 +823,7 @@ def publish_and_check_extras(extras) let(:client_options) { { key: api_key, environment: environment, protocol: :json } } let(:channel_name) { "subscribe_send_text-#{random_str}" } - fixtures_path = File.expand_path('../../../../lib/submodules/ably-common/test-resources/messages-encoding.json', __FILE__) + fixtures_path = File.expand_path('../../../../core/lib/submodules/ably-common/test-resources/messages-encoding.json', __FILE__) context 'over a JSON transport' do let(:realtime_client) do diff --git a/spec/acceptance/rest/message_spec.rb b/spec/acceptance/rest/message_spec.rb index 49960226e..b9ac0ac67 100644 --- a/spec/acceptance/rest/message_spec.rb +++ b/spec/acceptance/rest/message_spec.rb @@ -395,7 +395,7 @@ def mock_for_two_publish_failures end end - resources_root = File.expand_path('../../../../lib/submodules/ably-common/test-resources', __FILE__) + resources_root = File.expand_path('../../../../core/lib/submodules/ably-common/test-resources', __FILE__) def self.add_tests_for_data(data) data['items'].each_with_index do |item, index| diff --git a/spec/support/test_app.rb b/spec/support/test_app.rb index 2e7fc3944..89b1b5cfc 100644 --- a/spec/support/test_app.rb +++ b/spec/support/test_app.rb @@ -1,7 +1,7 @@ require 'singleton' class TestApp - TEST_RESOURCES_PATH = File.expand_path('../../../lib/submodules/ably-common/test-resources', __FILE__) + TEST_RESOURCES_PATH = File.expand_path('../../../core/lib/submodules/ably-common/test-resources', __FILE__) # App configuration for test app # See https://github.com/ably/ably-common/blob/main/test-resources/test-app-setup.json diff --git a/spec/unit/pubsub/server_spec.rb b/spec/unit/pubsub/server_spec.rb new file mode 100644 index 000000000..10c126328 --- /dev/null +++ b/spec/unit/pubsub/server_spec.rb @@ -0,0 +1,97 @@ +# encoding: utf-8 +require 'spec_helper' +require 'ably/pubsub/server' + +# The agent value asserted here is what MAU billing classification reads (PDR-091). +# These specs must fail loudly if the side-declaring entry is renamed, dropped, or +# becomes overridable by the caller. +describe Ably::PubSub::Server do + let(:api_key) { 'appid.keyuid:keysecret' } + let(:side_entry) { "ably-pubsub-server/#{Ably::PubSub::Server::VERSION}" } + + it 'releases in lockstep with ably-pubsub-core' do + expect(Ably::PubSub::Server::VERSION).to eql(Ably::VERSION) + end + + it 'declares an identifier whose -server suffix billing classification depends on' do + expect(Ably::PubSub::Server::SERVER_AGENT_IDENTIFIER).to match(/-server\z/) + end + + describe '.create_http_client' do + subject(:client) { Ably::PubSub::Server.create_http_client(api_key) } + + it 'returns an HTTP (REST) client' do + expect(client).to be_a(Ably::Rest::Client) + end + + it 'appends the side-declaring agent entry to the base agent' do + expect(client.agent).to eql("#{Ably::AGENT} #{side_entry}") + end + + it 'sends the side-declaring agent entry in the Ably-Agent header' do + expect(client.send(:connection_options)[:headers]['Ably-Agent']).to eql("#{Ably::AGENT} #{side_entry}") + end + + context 'with an options hash' do + subject(:client) { Ably::PubSub::Server.create_http_client(key: api_key, environment: 'sandbox') } + + it 'passes the options through and stamps the side' do + expect(client.environment).to eql('sandbox') + expect(client.agent).to end_with(side_entry) + end + end + + context 'with a token string' do + subject(:client) { Ably::PubSub::Server.create_http_client('tokenstring') } + + it 'constructs a token-auth client with the side stamped' do + expect(client.auth.options[:token]).to eql('tokenstring') + expect(client.agent).to end_with(side_entry) + end + end + + context 'when the caller supplies their own agents' do + subject(:client) { Ably::PubSub::Server.create_http_client(key: api_key, agents: { 'example-sdk' => '1.0.0' }) } + + it 'preserves the caller entries and appends the side entry' do + expect(client.agent).to eql("#{Ably::AGENT} example-sdk/1.0.0 #{side_entry}") + end + end + + context 'when the caller tries to override the side entry' do + subject(:client) { Ably::PubSub::Server.create_http_client(key: api_key, agents: { 'ably-pubsub-server' => 'not-the-version' }) } + + it 'the package wins the collision on its own identifier' do + expect(client.agent).to include(side_entry) + expect(client.agent).to_not include('not-the-version') + end + + it 'does not mutate the caller options' do + options = { key: api_key, agents: { 'example-sdk' => '1.0.0' } } + Ably::PubSub::Server.create_http_client(options) + expect(options[:agents]).to eql({ 'example-sdk' => '1.0.0' }) + end + end + end + + describe '.create_realtime_client' do + subject(:client) { Ably::PubSub::Server.create_realtime_client(auto_connect: false, key: api_key) } + + it 'returns a realtime client' do + expect(client).to be_a(Ably::Realtime::Client) + end + + it 'appends the side-declaring agent entry sent as the realtime agent connection param' do + # Ably::Realtime::Connection sends client.rest_client.agent as the `agent` param + expect(client.rest_client.agent).to eql("#{Ably::AGENT} #{side_entry}") + end + + context 'with an API key string' do + subject(:client) { Ably::PubSub::Server.create_realtime_client(api_key) } + + it 'constructs a key-auth client with the side stamped' do + expect(client.rest_client.agent).to end_with(side_entry) + end + end + end +end diff --git a/spec/unit/util/crypto_spec.rb b/spec/unit/util/crypto_spec.rb index d3a7729e9..56abc1da0 100644 --- a/spec/unit/util/crypto_spec.rb +++ b/spec/unit/util/crypto_spec.rb @@ -96,7 +96,7 @@ end context 'using shared client lib fixture data' do - let(:resources_root) { File.expand_path('../../../../lib/submodules/ably-common/test-resources', __FILE__) } + let(:resources_root) { File.expand_path('../../../../core/lib/submodules/ably-common/test-resources', __FILE__) } let(:encryption_data_128) { JSON.parse(File.read(File.join(resources_root, 'crypto-data-128.json'))) } let(:encryption_data_256) { JSON.parse(File.read(File.join(resources_root, 'crypto-data-256.json'))) } From e694f240490c673a202f4e1ed4a99343a8c65697 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 1 Sep 2026 16:38:59 +0100 Subject: [PATCH 02/13] Fix agent param spec to accept multi-digit version segments Ruby 3.3.11 and similar patch versions no longer match the previous single-digit \d.\d.\d pattern, which made this spec fail on current CI runners regardless of this branch's changes. Co-Authored-By: Claude Fable 5 --- spec/acceptance/realtime/connection_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/acceptance/realtime/connection_spec.rb b/spec/acceptance/realtime/connection_spec.rb index 36ca720fd..be3f59a6b 100644 --- a/spec/acceptance/realtime/connection_spec.rb +++ b/spec/acceptance/realtime/connection_spec.rb @@ -2005,7 +2005,7 @@ def self.available_states it 'sends the lib version param agent (#RCS7d)' do expect(EventMachine).to receive(:connect) do |host, port, transport, object, url| uri = URI.parse(url) - expect(CGI::parse(uri.query)['agent'][0]).to match(/^ably-ruby\/\d\.\d\.\d ruby\/\d\.\d\.\d$/) + expect(CGI::parse(uri.query)['agent'][0]).to match(/^ably-ruby\/\d+\.\d+\.\d+ ruby\/\d+\.\d+\.\d+$/) stop_reactor end client From 8847f7b48fc0542e5450de2b641e5a06f2bb6af2 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 2 Sep 2026 11:22:49 +0100 Subject: [PATCH 03/13] Stamp the side-declaring agent entry without a version Matches the entry's registration in the ably-common agents registry (ably/ably-common#361): the side entry is a pure flag, like browser. Under lockstep versioning a version on the flag always duplicates the ably-ruby entry beside it, which keeps carrying identity, version and support status. Wire shape: ably-ruby/2.0.0 ruby/3.3.0 ably-pubsub-server Co-Authored-By: Claude Fable 5 --- server/lib/ably/pubsub/server.rb | 8 +++++++- spec/unit/pubsub/server_spec.rb | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/server/lib/ably/pubsub/server.rb b/server/lib/ably/pubsub/server.rb index 56b1ac346..c0cf04818 100644 --- a/server/lib/ably/pubsub/server.rb +++ b/server/lib/ably/pubsub/server.rb @@ -13,6 +13,12 @@ module Server # `-server`, and an identifier that is not yet in the ably-common registry is # classified by that suffix alone. Renaming it without preserving the suffix # silently reclassifies every client this package constructs. + # + # The entry is stamped WITHOUT a version, matching its registration in the + # ably-common agents registry (a pure flag, like `browser`): under lockstep + # versioning a version here always duplicates the ably-ruby entry beside it, + # which keeps carrying identity, version and support status. Wire shape: + # ably-ruby/2.0.0 ruby/3.3.0 ably-pubsub-server SERVER_AGENT_IDENTIFIER = 'ably-pubsub-server' class << self @@ -61,7 +67,7 @@ def options_with_side_agent(options) options.clone end - agents = options[:agents].to_h.merge(SERVER_AGENT_IDENTIFIER => VERSION) + agents = options[:agents].to_h.merge(SERVER_AGENT_IDENTIFIER => nil) options.merge(agents: agents) end end diff --git a/spec/unit/pubsub/server_spec.rb b/spec/unit/pubsub/server_spec.rb index 10c126328..452cd7c2e 100644 --- a/spec/unit/pubsub/server_spec.rb +++ b/spec/unit/pubsub/server_spec.rb @@ -7,7 +7,9 @@ # becomes overridable by the caller. describe Ably::PubSub::Server do let(:api_key) { 'appid.keyuid:keysecret' } - let(:side_entry) { "ably-pubsub-server/#{Ably::PubSub::Server::VERSION}" } + # The side entry is a versionless flag, matching its ably-common registration: the + # ably-ruby/x.y.z entry beside it carries identity and version (see ably-common#361). + let(:side_entry) { 'ably-pubsub-server' } it 'releases in lockstep with ably-pubsub-core' do expect(Ably::PubSub::Server::VERSION).to eql(Ably::VERSION) @@ -24,8 +26,9 @@ expect(client).to be_a(Ably::Rest::Client) end - it 'appends the side-declaring agent entry to the base agent' do + it 'appends the side-declaring agent entry to the base agent, versionless' do expect(client.agent).to eql("#{Ably::AGENT} #{side_entry}") + expect(client.agent).to_not include('ably-pubsub-server/') end it 'sends the side-declaring agent entry in the Ably-Agent header' do @@ -62,8 +65,9 @@ subject(:client) { Ably::PubSub::Server.create_http_client(key: api_key, agents: { 'ably-pubsub-server' => 'not-the-version' }) } it 'the package wins the collision on its own identifier' do - expect(client.agent).to include(side_entry) + expect(client.agent).to end_with(side_entry) expect(client.agent).to_not include('not-the-version') + expect(client.agent).to_not include('ably-pubsub-server/') end it 'does not mutate the caller options' do From d24039ba17aac6129a4d877491095b24257edb49 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 1 Sep 2026 17:04:44 +0100 Subject: [PATCH 04/13] Add lockstep release workflow, update contributing and migration docs - release.yml publishes ably-pubsub-core and ably-pubsub-server at the same version via RubyGems trusted publishing (OIDC, no long-lived keys). A pre-flight fails before anything is pushed if the version input, either gem's version constant, or the server gem's exact core pin disagree. Partial releases fail reversibly: re-running with the same version skips already-published gems. - CONTRIBUTING.md documents the two-gem layout and the new release process; the manual rake release and the ably-ruby-rest step are gone (both legacy gems are maintenance-only per PDR-091b). - UPDATING.md gains the 1.x -> 2.0 migration section with the old-name -> new-name mapping table, marked draft pending PDR-091d. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 101 ++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 41 ++++++++++---- UPDATING.md | 32 +++++++++++ 3 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..d7113e1f7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,101 @@ +name: Release + +# Releases ably-pubsub-core and ably-pubsub-server in lockstep at the same version +# (PDR-091b). The pre-flight fails before anything is pushed if the version input, +# the two gems' version files, or the server->core exact pin disagree. +# +# Publishing uses RubyGems trusted publishing (OIDC): both gems must have a Trusted +# Publisher configured on rubygems.org pointing at this repository and this workflow +# file. NOTE: the binding is to the repo owner+name, so it must be reconfigured when +# the repo is renamed to ably-pubsub-ruby. +# +# A partial release fails reversibly: if the server push fails after the core push +# succeeded, re-running the workflow with the same version skips the already-published +# core gem and publishes the server gem. + +on: + workflow_dispatch: + inputs: + version: + description: "Version to release, e.g. 2.0.0 — must match Ably::VERSION, Ably::PubSub::Server::VERSION and the server gemspec's core pin" + required: true + +permissions: {} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + with: + persist-credentials: false + + - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0 + with: + ruby-version: '3.3' + bundler-cache: false + + - name: 'Pre-flight: versions and pin must agree (nothing is pushed if this fails)' + env: + RELEASE_VERSION: ${{ github.event.inputs.version }} + run: | + ruby <<'RUBY' + version = ENV.fetch('RELEASE_VERSION') + abort "Invalid version input: #{version.inspect}" unless version.match?(/\A\d+\.\d+\.\d+(\.[0-9A-Za-z]+)*\z/) + + require_relative 'core/lib/ably/version' + require_relative 'server/lib/ably/pubsub/server/version' + + errors = [] + errors << "core Ably::VERSION is #{Ably::VERSION}, expected #{version}" unless Ably::VERSION == version + errors << "server Ably::PubSub::Server::VERSION is #{Ably::PubSub::Server::VERSION}, expected #{version}" unless Ably::PubSub::Server::VERSION == version + + server_spec = Gem::Specification.load('server/ably-pubsub-server.gemspec') + core_dep = server_spec.dependencies.find { |d| d.name == 'ably-pubsub-core' } + errors << "server gemspec pins ably-pubsub-core '#{core_dep&.requirement}', expected '= #{version}'" unless core_dep&.requirement.to_s == "= #{version}" + + abort errors.join("\n") unless errors.empty? + puts "Pre-flight OK: releasing ably-pubsub-core and ably-pubsub-server at #{version}" + RUBY + + - name: Configure RubyGems credentials (trusted publishing) + uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0 + + - name: Publish both gems in lockstep + env: + RELEASE_VERSION: ${{ github.event.inputs.version }} + run: | + set -euo pipefail + v="${RELEASE_VERSION}" + + published() { + curl -sf "https://rubygems.org/api/v2/rubygems/$1/versions/${v}.json" >/dev/null + } + + push_gem() { + local name="$1" dir="$2" + if published "${name}"; then + echo "${name} ${v} is already on RubyGems, skipping (safe re-run)" + return 0 + fi + (cd "${dir}" && gem build "${name}.gemspec") + gem push "${dir}/${name}-${v}.gem" + } + + push_gem ably-pubsub-core core + + # The server gem pins the core at this exact version, so wait until the + # core version is visible on RubyGems before publishing the server gem. + for i in $(seq 1 30); do + published ably-pubsub-core && break + echo "Waiting for ably-pubsub-core ${v} to appear on RubyGems (${i}/30)..." + sleep 10 + done + published ably-pubsub-core || { echo "ably-pubsub-core ${v} did not appear on RubyGems"; exit 1; } + + push_gem ably-pubsub-server server + + echo "Released ably-pubsub-core and ably-pubsub-server at ${v}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eac0af682..c086220de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,31 +1,48 @@ # Contributing +This repository hosts two gems, released in lockstep at the same version: + +- [`core/`](./core) — `ably-pubsub-core`: the shared implementation. An internal package; only Ably packages depend on it. +- [`server/`](./server) — `ably-pubsub-server`: the public server-side package. Its factory functions (`Ably::PubSub::Server.create_http_client` / `.create_realtime_client`) are the only recommended entry points. + +## Development + 1. Fork it 2. When pulling to local, make sure to also pull the `ably-common` repo (`git submodule init && git submodule update`) 3. Create your feature branch (`git checkout -b my-new-feature`) 4. Commit your changes (`git commit -am 'Add some feature'`) -5. Ensure you have added suitable tests and the test suite is passing(`bundle exec rspec`) +5. Ensure you have added suitable tests and the test suite is passing (`bundle exec rspec`) — the root `Gemfile` wires both gems up as path dependencies, so a single `bundle install` at the root covers everything 6. Push to the branch (`git push origin my-new-feature`) 7. Create a new Pull Request ---- - ## Release process -This library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done: +This library uses [semantic versioning](http://semver.org/). `ably-pubsub-core` and `ably-pubsub-server` always release together at the same version: the release workflow refuses to publish them independently. + +For each release, the following needs to be done: -1. Create a branch for the release, named like `release/1.2.3` (where `1.2.3` is the new version number) -2. Update the version number in [version.rb](./lib/ably/version.rb) and commit the change. +1. Create a branch for the release, named like `release/2.0.1` (where `2.0.1` is the new version number) +2. Update the version number in **all three places**, which must agree (the release workflow's pre-flight enforces this): + - `Ably::VERSION` in [core/lib/ably/version.rb](./core/lib/ably/version.rb) + - `Ably::PubSub::Server::VERSION` in [server/lib/ably/pubsub/server/version.rb](./server/lib/ably/pubsub/server/version.rb) + - the exact-version `ably-pubsub-core` pin in [server/ably-pubsub-server.gemspec](./server/ably-pubsub-server.gemspec) (derived from the version constant, so it normally follows automatically) 3. Run [`github_changelog_generator`](https://github.com/github-changelog-generator/github-changelog-generator) to automate the update of the [CHANGELOG](./CHANGELOG.md). This may require some manual intervention, both in terms of how the command is run and how the change log file is modified. Your mileage may vary: - - The command you will need to run will look something like this: `github_changelog_generator -u ably -p ably-ruby --since-tag v1.2.3 --output delta.md --token $GITHUB_TOKEN_WITH_REPO_ACCESS`. Generate token [here](https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token). + - The command you will need to run will look something like this: `github_changelog_generator -u ably -p ably-ruby --since-tag v2.0.0 --output delta.md --token $GITHUB_TOKEN_WITH_REPO_ACCESS`. Generate token [here](https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token). - Using the command above, `--output delta.md` writes changes made after `--since-tag` to a new file - The contents of that new file (`delta.md`) then need to be manually inserted at the top of the `CHANGELOG.md`, changing the "Unreleased" heading and linking with the current version numbers - Also ensure that the "Full Changelog" link points to the new version tag instead of the `HEAD` 4. Commit this change: `git add CHANGELOG.md && git commit -m "Update change log."` 5. Ideally, run `rake doc:spec` to generate a new [spec file](./SPEC.md). Then commit these changes. 6. Make a PR against `main`. Once the PR is approved, merge it into `main`. -7. Add a tag to the new `main` head commit and push to origin such as `git tag v1.0.3 && git push origin v1.0.3`. -8. Visit [https://github.com/ably/ably-ruby/tags](https://github.com/ably/ably-ruby/tags) and `Add release notes` for the release including links to the changelog entry. -9. Run `rake release` to publish the gem to [Rubygems](https://rubygems.org/gems/ably). -10. Release the [REST-only library `ably-ruby-rest`](https://github.com/ably/ably-ruby-rest#release-process). -11. Create the entry on the [Ably Changelog](https://changelog.ably.com/) (via [headwayapp](https://headwayapp.co/)). +7. Add a tag to the new `main` head commit and push to origin such as `git tag v2.0.1 && git push origin v2.0.1`. +8. Visit [the tags page](https://github.com/ably/ably-ruby/tags) and `Add release notes` for the release including links to the changelog entry. +9. Run the [Release workflow](./.github/workflows/release.yml) (Actions → Release → Run workflow) with the version number. It publishes `ably-pubsub-core` and then `ably-pubsub-server` to RubyGems via trusted publishing — no local credentials involved. A failed run is safe to re-run with the same version: already-published gems are skipped. +10. Create the entry on the [Ably Changelog](https://changelog.ably.com/) (via [headwayapp](https://headwayapp.co/)). + +### Trusted publishing + +The workflow authenticates to RubyGems with [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (GitHub OIDC): both gems have a Trusted Publisher configured on rubygems.org bound to this repository and `.github/workflows/release.yml`. There are no long-lived RubyGems API keys anywhere. If the repository is renamed, both bindings must be reconfigured on rubygems.org or publishing fails. + +### The legacy `ably` gem + +The `ably` gem is in its maintenance window (security and critical fixes only, released from the maintenance branch) and is **not** released from `main`. The `ably-rest` gem (from the `ably-ruby-rest` repo) is likewise in maintenance and no longer part of this release process. diff --git a/UPDATING.md b/UPDATING.md index db9ad6f76..fabb77aff 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,5 +1,37 @@ # Upgrade / Migration Guide +## Version 1.x (`ably` gem) to 2.0.0 (`ably-pubsub-server` gem) + +> **Status: draft.** The final public API naming is still under review; this section will be finalized before the 2.0.0 GA release. + +Version 2.0.0 splits the SDK into new packages. The `ably` gem is superseded: it receives security and critical-bug fixes only for one year from the 2.0.0 release date, and is then end-of-life. Under MAU-based pricing the platform must classify every connection as device- or server-side; the new packages declare this automatically, while the old constructors cannot — once MAU pricing is live, they raise on MAU-enabled accounts. + +Ruby is a server-side SDK, so there is a single new public gem, `ably-pubsub-server`, whose factory functions are the only recommended entry points. (It is built on `ably-pubsub-core`, an internal gem you should never depend on directly.) The objects the factories return are the same clients as today — channels, presence, history, auth and error handling are unchanged. For most applications the migration is confined to the Gemfile, the `require`, and the constructor call. + +### Mapping + +| 1.x (`ably`) | 2.0 (`ably-pubsub-server`) | +| --- | --- | +| `gem 'ably'` | `gem 'ably-pubsub-server'` | +| `gem 'ably-rest'` (from `ably-ruby-rest`) | `gem 'ably-pubsub-server'` | +| `require 'ably'` | `require 'ably/pubsub/server'` | +| `Ably::Rest::Client.new(options)` | `Ably::PubSub::Server.create_http_client(options)` | +| `Ably::Realtime::Client.new(options)` | `Ably::PubSub::Server.create_realtime_client(options)` | + +### Example + +```ruby +# 1.x +require 'ably' +client = Ably::Rest::Client.new(key: ENV['ABLY_API_KEY']) + +# 2.0 +require 'ably/pubsub/server' +client = Ably::PubSub::Server.create_http_client(key: ENV['ABLY_API_KEY']) +``` + +Both factories accept everything the old constructors accepted: an options `Hash`, an API key `String`, or a token `String`. + ## Version 1.1.8 to 1.2.0 ### Notable Changes From 79cde4c7b863a81fa38d2149cde23a31ab33417d Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 1 Sep 2026 17:13:14 +0100 Subject: [PATCH 05/13] Add release dry-run job to CI Runs the same version-consistency assertions as release.yml's pre-flight and builds both gems on every PR, so a version-file or gemspec regression surfaces immediately instead of at release time. Publishes nothing. Co-Authored-By: Claude Fable 5 --- .github/workflows/check.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c10d05282..28959a303 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -56,6 +56,43 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: ruby-${{ matrix.ruby }}-${{ matrix.protocol }}-${{ matrix.type }} parallel: true + # Dry run of the release pipeline: the same consistency assertions release.yml's + # pre-flight makes, plus a build of both gems — so a version-file or gemspec + # regression surfaces on every PR instead of at release time. Publishes nothing. + release-dry-run: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + with: + persist-credentials: false + - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0 + with: + ruby-version: '3.3' + bundler-cache: false + - name: 'Versions and pin must agree (mirrors release.yml pre-flight)' + run: | + ruby <<'RUBY' + require_relative 'core/lib/ably/version' + require_relative 'server/lib/ably/pubsub/server/version' + + errors = [] + errors << "core Ably::VERSION (#{Ably::VERSION}) != server Ably::PubSub::Server::VERSION (#{Ably::PubSub::Server::VERSION})" unless Ably::VERSION == Ably::PubSub::Server::VERSION + + server_spec = Gem::Specification.load('server/ably-pubsub-server.gemspec') + core_dep = server_spec.dependencies.find { |d| d.name == 'ably-pubsub-core' } + errors << "server gemspec pins ably-pubsub-core '#{core_dep&.requirement}', expected '= #{Ably::VERSION}'" unless core_dep&.requirement.to_s == "= #{Ably::VERSION}" + + abort errors.join("\n") unless errors.empty? + puts "Version consistency OK: #{Ably::VERSION}" + RUBY + - name: Build both gems + run: | + set -euo pipefail + (cd core && gem build ably-pubsub-core.gemspec) + (cd server && gem build ably-pubsub-server.gemspec) + finish: needs: check runs-on: ubuntu-latest From 11b33308e234ea9bb7059811c5f79de236ce5cff Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 2 Sep 2026 12:52:05 +0100 Subject: [PATCH 06/13] Rename the SDK agent identifier to ably-pubsub-ruby The family identifier follows the package split, per the agent identifier convention proposed on PDR-091b2 and registered in ably/ably-common#361: because it flips exactly at the split and the maintenance branch is never touched, the identifier alone partitions the fleet - ably-ruby/* is legacy-gem traffic, ably-pubsub-ruby/* is new-package traffic. It names the family rather than any one published gem; the side a client declares travels as the separate versionless entry stamped by the server factories: ably-pubsub-ruby/2.0.0 ruby/3.3.0 ably-pubsub-server Requires the ably-pubsub-ruby registry entry (ably/ably-common#361) before any release from this branch. Mirrors ably/ably-js#2297 and ably/ably-java#1233. Co-Authored-By: Claude Fable 5 --- core/lib/ably/agent.rb | 7 ++++++- core/lib/ably/rest/client.rb | 2 +- server/lib/ably/pubsub/server.rb | 4 ++-- spec/acceptance/realtime/connection_spec.rb | 2 +- spec/unit/pubsub/server_spec.rb | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/lib/ably/agent.rb b/core/lib/ably/agent.rb index 39ea39e09..c4442ef73 100644 --- a/core/lib/ably/agent.rb +++ b/core/lib/ably/agent.rb @@ -1,3 +1,8 @@ module Ably - AGENT = "ably-ruby/#{Ably::VERSION} ruby/#{RUBY_VERSION}" + # The SDK family identifier renamed from `ably-ruby` with the per-side package split, so the + # identifier alone partitions the fleet: `ably-ruby/*` is legacy-gem traffic, + # `ably-pubsub-ruby/*` is new-package traffic. It names the family rather than any one + # published gem; the side a client declares travels as a separate versionless agent entry + # (see Ably::PubSub::Server and the agents registry in ably-common). + AGENT = "ably-pubsub-ruby/#{Ably::VERSION} ruby/#{RUBY_VERSION}" end diff --git a/core/lib/ably/rest/client.rb b/core/lib/ably/rest/client.rb index 523f314ec..04e60a5be 100644 --- a/core/lib/ably/rest/client.rb +++ b/core/lib/ably/rest/client.rb @@ -51,7 +51,7 @@ class Client # @return [Symbol] attr_reader :protocol - # Client agent i.e. `example-gem/1.2.0 ably-ruby/1.1.5 ruby/3.1.1` + # Client agent i.e. `example-gem/1.2.0 ably-pubsub-ruby/1.1.5 ruby/3.1.1` # @return [String] attr_reader :agent diff --git a/server/lib/ably/pubsub/server.rb b/server/lib/ably/pubsub/server.rb index c0cf04818..436546cb1 100644 --- a/server/lib/ably/pubsub/server.rb +++ b/server/lib/ably/pubsub/server.rb @@ -16,9 +16,9 @@ module Server # # The entry is stamped WITHOUT a version, matching its registration in the # ably-common agents registry (a pure flag, like `browser`): under lockstep - # versioning a version here always duplicates the ably-ruby entry beside it, + # versioning a version here always duplicates the ably-pubsub-ruby entry beside it, # which keeps carrying identity, version and support status. Wire shape: - # ably-ruby/2.0.0 ruby/3.3.0 ably-pubsub-server + # ably-pubsub-ruby/2.0.0 ruby/3.3.0 ably-pubsub-server SERVER_AGENT_IDENTIFIER = 'ably-pubsub-server' class << self diff --git a/spec/acceptance/realtime/connection_spec.rb b/spec/acceptance/realtime/connection_spec.rb index be3f59a6b..d23b0cadb 100644 --- a/spec/acceptance/realtime/connection_spec.rb +++ b/spec/acceptance/realtime/connection_spec.rb @@ -2005,7 +2005,7 @@ def self.available_states it 'sends the lib version param agent (#RCS7d)' do expect(EventMachine).to receive(:connect) do |host, port, transport, object, url| uri = URI.parse(url) - expect(CGI::parse(uri.query)['agent'][0]).to match(/^ably-ruby\/\d+\.\d+\.\d+ ruby\/\d+\.\d+\.\d+$/) + expect(CGI::parse(uri.query)['agent'][0]).to match(/^ably-pubsub-ruby\/\d+\.\d+\.\d+ ruby\/\d+\.\d+\.\d+$/) stop_reactor end client diff --git a/spec/unit/pubsub/server_spec.rb b/spec/unit/pubsub/server_spec.rb index 452cd7c2e..9b2f20f2e 100644 --- a/spec/unit/pubsub/server_spec.rb +++ b/spec/unit/pubsub/server_spec.rb @@ -8,7 +8,7 @@ describe Ably::PubSub::Server do let(:api_key) { 'appid.keyuid:keysecret' } # The side entry is a versionless flag, matching its ably-common registration: the - # ably-ruby/x.y.z entry beside it carries identity and version (see ably-common#361). + # ably-pubsub-ruby/x.y.z entry beside it carries identity and version (see ably-common#361). let(:side_entry) { 'ably-pubsub-server' } it 'releases in lockstep with ably-pubsub-core' do From f18bcbe5698e5d24b7489d45484cfe989863d60f Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 2 Sep 2026 13:17:52 +0100 Subject: [PATCH 07/13] Trim release workflow header comment Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7113e1f7..bff4a22ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,12 @@ name: Release -# Releases ably-pubsub-core and ably-pubsub-server in lockstep at the same version -# (PDR-091b). The pre-flight fails before anything is pushed if the version input, -# the two gems' version files, or the server->core exact pin disagree. +# Releases ably-pubsub-core and ably-pubsub-server in lockstep at the same version. +# The pre-flight fails before anything is pushed if the version input, the two gems' +# version files, or the server->core exact pin disagree. # # Publishing uses RubyGems trusted publishing (OIDC): both gems must have a Trusted # Publisher configured on rubygems.org pointing at this repository and this workflow -# file. NOTE: the binding is to the repo owner+name, so it must be reconfigured when -# the repo is renamed to ably-pubsub-ruby. +# file. # # A partial release fails reversibly: if the server push fails after the core push # succeeded, re-running the workflow with the same version skips the already-published From 97f823f00ff1db40da4a54848b74badcf79d913d Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 2 Sep 2026 13:38:15 +0100 Subject: [PATCH 08/13] Add gem-packaging specs for the core/server split Asserts the two gemspecs' built file lists directly: no load-path file ships in both gems, core does not ship the server subtree or the ably-common submodule, server ships only its own subtree under lib, and the versions and server->core pin agree. The release pre-flight checks version agreement, but nothing else asserts gem contents, so a packaging mistake would otherwise surface only after publish. Adapted from the packaging specs in #452. Co-Authored-By: Claude Fable 5 --- spec/unit/pubsub/packaging_spec.rb | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 spec/unit/pubsub/packaging_spec.rb diff --git a/spec/unit/pubsub/packaging_spec.rb b/spec/unit/pubsub/packaging_spec.rb new file mode 100644 index 000000000..7eb1675bc --- /dev/null +++ b/spec/unit/pubsub/packaging_spec.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 +require 'spec_helper' + +# The Ably namespace is assembled at install time from two gems: ably-pubsub-core ships the +# implementation under lib/ably, and ably-pubsub-server ships only the lib/ably/pubsub/server +# subtree on top of it. That only holds together if each gem ships exactly its own subtree — +# a file shipped by both would be resolved from whichever gem comes first on the load path, +# hiding the other's copy. The release pre-flight checks version agreement but nothing else +# asserts the gems' file lists, so a packaging mistake would otherwise surface only after +# publish. These specs load the gemspecs and check the built file lists directly. +describe 'Pub/Sub gem packaging' do + repo_root = File.expand_path('../../..', __dir__) + + gemspec_for = lambda do |gem_name, dir| + path = File.join(repo_root, dir, "#{gem_name}.gemspec") + Gem::Specification.load(path) || raise("could not load #{path}") + end + + core_spec = gemspec_for.call('ably-pubsub-core', 'core') + server_spec = gemspec_for.call('ably-pubsub-server', 'server') + + it 'ships no load-path file in both gems' do + core_lib = core_spec.files.grep(%r{\Alib/}) + server_lib = server_spec.files.grep(%r{\Alib/}) + expect(core_lib & server_lib).to be_empty + end + + it 'core does not ship the server subtree' do + expect(core_spec.files.grep(%r{\Alib/ably/pubsub(/|\.rb\z)})).to be_empty + end + + it 'server ships only the lib/ably/pubsub/server subtree under lib' do + lib_files = server_spec.files.grep(%r{\Alib/}) + expect(lib_files).to_not be_empty + expect(lib_files).to all(match(%r{\Alib/ably/pubsub/server(/|\.rb\z)})) + end + + it 'core does not ship the ably-common submodule' do + expect(core_spec.files.grep(%r{\Alib/submodules/})).to be_empty + end + + it 'releases both gems at one version (lockstep)' do + expect(server_spec.version).to eql(core_spec.version) + end + + it 'server pins core at exactly the shared version' do + core_dep = server_spec.dependencies.find { |dep| dep.name == 'ably-pubsub-core' } + expect(core_dep).to_not be_nil + expect(core_dep.requirement.to_s).to eql("= #{core_spec.version}") + end +end From b57ccd568ad70e6973ef1831d20ff199708dee57 Mon Sep 17 00:00:00 2001 From: umair Date: Thu, 3 Sep 2026 09:55:50 +0100 Subject: [PATCH 09/13] Update server gemspec homepage for the repo rename Co-Authored-By: Claude Fable 5 --- server/ably-pubsub-server.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ably-pubsub-server.gemspec b/server/ably-pubsub-server.gemspec index 57cfcbb54..8b8fb157e 100644 --- a/server/ably-pubsub-server.gemspec +++ b/server/ably-pubsub-server.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.email = ['support@ably.com'] spec.description = %q{Ably Pub/Sub client for servers: backend services and other trusted runtimes. Construct clients with Ably::PubSub::Server.create_http_client or Ably::PubSub::Server.create_realtime_client.} spec.summary = %q{Ably Pub/Sub client for servers} - spec.homepage = 'https://github.com/ably/ably-ruby' + spec.homepage = 'https://github.com/ably/ably-pubsub-ruby' spec.license = 'Apache-2.0' spec.files = Dir.chdir(File.expand_path(__dir__)) { `git ls-files -z lib README.md`.split("\x0") } From ddc408a2bca9c81d1deb553895bc1c00c56c7f2a Mon Sep 17 00:00:00 2001 From: umair Date: Fri, 4 Sep 2026 12:41:16 +0100 Subject: [PATCH 10/13] Signpost the old ably gem's API reference from the new docs front page ably.com/docs links only the newest package's API reference, so users still on the maintenance-only ably gem land here; the front-page notice points them at the frozen old reference and the migration guide. Co-Authored-By: Claude Fable 5 --- INTRO.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/INTRO.md b/INTRO.md index 582a0a85f..d4ec47904 100644 --- a/INTRO.md +++ b/INTRO.md @@ -1,5 +1,10 @@ # Ably `Ruby` Client Library SDK API Reference +> **Using the `ably` gem?** This is the API reference for the `ably-pubsub-server` gem, which replaces it. +> The API reference for the maintenance-only `ably` gem remains available at +> [sdk.ably.com/builds/ably/ably-ruby/main/docs](https://sdk.ably.com/builds/ably/ably-ruby/main/docs/) +> until its end of life. + The `Ruby` Client Library SDK supports a realtime and a REST interface. The realtime interface enables a client to maintain a persistent connection to Ably and publish, subscribe and be present on channels. From 6a8a6d573df9aef38a598135b16b3a36db821dbb Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 14 Sep 2026 21:22:17 +0100 Subject: [PATCH 11/13] Refactor client instantiation to enforce side-specific construction through `Ably::Internal`. --- CONTRIBUTING.md | 2 +- README.md | 6 +- UPDATING.md | 4 +- core/README.md | 10 ++- core/lib/ably.rb | 3 + core/lib/ably/auth.rb | 4 +- core/lib/ably/exceptions.rb | 5 ++ core/lib/ably/internal.rb | 64 +++++++++++++++ core/lib/ably/realtime.rb | 28 +++---- core/lib/ably/realtime/auth.rb | 4 +- core/lib/ably/realtime/client.rb | 24 ++++-- core/lib/ably/realtime/connection.rb | 2 +- core/lib/ably/rest.rb | 28 +++---- core/lib/ably/rest/client.rb | 20 ++++- server/README.md | 5 +- server/lib/ably/pubsub/server.rb | 9 ++- spec/acceptance/realtime/auth_spec.rb | 18 ++--- .../realtime/channel_history_spec.rb | 4 +- spec/acceptance/realtime/channel_spec.rb | 40 +++++----- spec/acceptance/realtime/channels_spec.rb | 2 +- spec/acceptance/realtime/client_spec.rb | 20 ++--- .../realtime/connection_failures_spec.rb | 8 +- spec/acceptance/realtime/connection_spec.rb | 32 ++++---- spec/acceptance/realtime/message_spec.rb | 24 +++--- .../realtime/presence_history_spec.rb | 4 +- spec/acceptance/realtime/presence_spec.rb | 60 +++++++------- spec/acceptance/realtime/push_admin_spec.rb | 2 +- spec/acceptance/realtime/stats_spec.rb | 2 +- spec/acceptance/realtime/time_spec.rb | 4 +- spec/acceptance/rest/auth_spec.rb | 28 +++---- spec/acceptance/rest/base_spec.rb | 8 +- spec/acceptance/rest/channel_spec.rb | 18 ++--- spec/acceptance/rest/channels_spec.rb | 2 +- spec/acceptance/rest/client_spec.rb | 26 +++--- spec/acceptance/rest/encoders_spec.rb | 2 +- spec/acceptance/rest/message_spec.rb | 12 +-- spec/acceptance/rest/presence_spec.rb | 6 +- spec/acceptance/rest/push_admin_spec.rb | 2 +- spec/acceptance/rest/stats_spec.rb | 2 +- spec/acceptance/rest/time_spec.rb | 4 +- spec/support/test_app.rb | 4 +- spec/unit/pubsub/direct_construction_spec.rb | 80 +++++++++++++++++++ spec/unit/pubsub/packaging_spec.rb | 21 +++++ spec/unit/realtime/channel_spec.rb | 2 +- spec/unit/realtime/client_spec.rb | 4 +- spec/unit/realtime/realtime_spec.rb | 11 ++- spec/unit/rest/client_spec.rb | 2 +- spec/unit/rest/rest_spec.rb | 7 +- 48 files changed, 443 insertions(+), 236 deletions(-) create mode 100644 core/lib/ably/internal.rb create mode 100644 spec/unit/pubsub/direct_construction_spec.rb diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 978685871..e7ad71fd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ This repository hosts two gems, released in lockstep at the same version: - [`core/`](./core) — `ably-pubsub-core`: the shared implementation. An internal package; only Ably packages depend on it. -- [`server/`](./server) — `ably-pubsub-server`: the public server-side package. Its factory functions (`Ably::PubSub::Server.create_http_client` / `.create_realtime_client`) are the only recommended entry points. +- [`server/`](./server) — `ably-pubsub-server`: the public server-side package. Its factory functions (`Ably::PubSub::Server.create_http_client` / `.create_realtime_client`) are the only entry points: the client classes refuse direct construction, and these factories reach them through `Ably::Internal`. ## Development diff --git a/README.md b/README.md index 001057bf7..17d399b03 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,10 @@ To use the Ably Realtime SDK in Ruby, the `EventMachine` reactor loop must be ru Wrap your code inside a `EventMachine.run` block: ```ruby -require 'ably' +require 'ably/pubsub/server' EventMachine.run do - client = Ably::Realtime.new(key: 'your-api-key') + client = Ably::PubSub::Server.create_realtime_client(key: 'your-api-key') client.connection.connect do puts "Connected with connection ID: #{client.connection.id}" @@ -87,7 +87,7 @@ The following code connects to Ably's realtime messaging service, subscribes to ```ruby # Initialize Ably Realtime client - realtime_client = Ably::Realtime.new(key: 'your-ably-api-key', client_id: 'me') + realtime_client = Ably::PubSub::Server.create_realtime_client(key: 'your-ably-api-key', client_id: 'me') # Wait for connection to be established realtime_client.connection.on(:connected) do diff --git a/UPDATING.md b/UPDATING.md index fabb77aff..524548b7b 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -6,7 +6,7 @@ Version 2.0.0 splits the SDK into new packages. The `ably` gem is superseded: it receives security and critical-bug fixes only for one year from the 2.0.0 release date, and is then end-of-life. Under MAU-based pricing the platform must classify every connection as device- or server-side; the new packages declare this automatically, while the old constructors cannot — once MAU pricing is live, they raise on MAU-enabled accounts. -Ruby is a server-side SDK, so there is a single new public gem, `ably-pubsub-server`, whose factory functions are the only recommended entry points. (It is built on `ably-pubsub-core`, an internal gem you should never depend on directly.) The objects the factories return are the same clients as today — channels, presence, history, auth and error handling are unchanged. For most applications the migration is confined to the Gemfile, the `require`, and the constructor call. +Ruby is a server-side SDK, so there is a single new public gem, `ably-pubsub-server`, whose factory functions are the only entry points: the old constructors raise `Ably::Exceptions::DirectConstructionNotSupported`, because the package a client is created from is what declares its side and a directly constructed client declares none. ### Mapping @@ -16,7 +16,9 @@ Ruby is a server-side SDK, so there is a single new public gem, `ably-pubsub-ser | `gem 'ably-rest'` (from `ably-ruby-rest`) | `gem 'ably-pubsub-server'` | | `require 'ably'` | `require 'ably/pubsub/server'` | | `Ably::Rest::Client.new(options)` | `Ably::PubSub::Server.create_http_client(options)` | +| `Ably::Rest.new(options)` | `Ably::PubSub::Server.create_http_client(options)` | | `Ably::Realtime::Client.new(options)` | `Ably::PubSub::Server.create_realtime_client(options)` | +| `Ably::Realtime.new(options)` | `Ably::PubSub::Server.create_realtime_client(options)` | ### Example diff --git a/core/README.md b/core/README.md index 382d9e8f8..94c4c7966 100644 --- a/core/README.md +++ b/core/README.md @@ -3,8 +3,14 @@ Internal implementation package for Ably's own Pub/Sub Ruby packages. **This gem is not intended for direct external use.** It is published only so that -Ably's public packages can depend on it. Use [`ably-pubsub-server`](../server) instead, -which exposes the supported entry points: +Ably's public packages can depend on it, and it constructs nothing on its own: +`Ably::Rest::Client.new` and `Ably::Realtime::Client.new` raise +`Ably::Exceptions::DirectConstructionNotSupported`. The package a client is created from +is what declares its side to the platform, and a directly constructed client declares +none. Ably's per-side packages construct clients through `Ably::Internal`, which is not +public API. + +Use [`ably-pubsub-server`](../server) instead, which exposes the supported entry points: ```ruby client = Ably::PubSub::Server.create_http_client(key) # stateless HTTP client diff --git a/core/lib/ably.rb b/core/lib/ably.rb index a1ef97330..3e1e96ff0 100644 --- a/core/lib/ably.rb +++ b/core/lib/ably.rb @@ -14,3 +14,6 @@ require 'ably/logger' require 'ably/realtime' require 'ably/rest' + +# Requires the client classes above: it constructs them. +require 'ably/internal' diff --git a/core/lib/ably/auth.rb b/core/lib/ably/auth.rb index 06b4598d9..764d0b09b 100644 --- a/core/lib/ably/auth.rb +++ b/core/lib/ably/auth.rb @@ -123,7 +123,7 @@ def initialize(client, token_params, auth_options) # # @example # # will issue a simple token request using basic auth - # client = Ably::Rest::Client.new(key: 'key.id:secret') + # client = Ably::PubSub::Server.create_http_client(key: 'key.id:secret') # token_details = client.auth.authorize # # # will use token request from block to authorize if not already authorized @@ -214,7 +214,7 @@ def authorise(*args, &block) # # @example # # simple token request using basic auth - # client = Ably::Rest::Client.new(key: 'key.id:secret') + # client = Ably::PubSub::Server.create_http_client(key: 'key.id:secret') # token_details = client.auth.request_token # # # token request with token params diff --git a/core/lib/ably/exceptions.rb b/core/lib/ably/exceptions.rb index 03823c396..6bc0753c2 100644 --- a/core/lib/ably/exceptions.rb +++ b/core/lib/ably/exceptions.rb @@ -165,5 +165,10 @@ def initialize(messages, status = 400, code = Ably::Exceptions::Codes::INVALID_C class InvalidTokenRequest < BaseAblyException; end class PushNotificationsNotSupported < BaseAblyException; end + + # A client class was constructed directly rather than through the factory function of + # an Ably per-side package. The package a client is created from is what declares the + # client's side to the platform, so a directly constructed client declares none. + class DirectConstructionNotSupported < BaseAblyException; end end end diff --git a/core/lib/ably/internal.rb b/core/lib/ably/internal.rb new file mode 100644 index 000000000..3e9f21f54 --- /dev/null +++ b/core/lib/ably/internal.rb @@ -0,0 +1,64 @@ +module Ably + # Construction entry points for Ably's own per-side packages: +ably-pubsub-server+, and + # any future device-side package. + # + # Applications must not use this module. It carries no compatibility guarantee and may + # change in any release, including a patch. Use the factory functions of the package you + # installed — see {Ably::PubSub::Server} in the +ably-pubsub-server+ gem. + # + # The client classes refuse direct construction ({Ably::Rest::Client.new} and + # {Ably::Realtime::Client.new} raise {Ably::Exceptions::DirectConstructionNotSupported}), + # because the package a client is created from is what declares the client's side to the + # platform and a directly constructed client declares none. These entry points are how a + # per-side package constructs the client it has stamped its side onto. + module Internal + class << self + # Constructs an {Ably::Rest::Client}, bypassing the direct-construction refusal. + # + # @param options [Hash, String] as accepted by {Ably::Rest::Client#initialize} + # @return [Ably::Rest::Client] + # @api private + def create_rest_client(options) + construct(Ably::Rest::Client, options) + end + + # Constructs an {Ably::Realtime::Client}, bypassing the direct-construction refusal. + # + # @param options [Hash, String] as accepted by {Ably::Realtime::Client#initialize} + # @return [Ably::Realtime::Client] + # @api private + def create_realtime_client(options) + construct(Ably::Realtime::Client, options) + end + + # Builds the error raised when a client is constructed directly. Shared so the + # refusals on the client classes and on the Ably::Rest / Ably::Realtime convenience + # aliases all speak with one voice. + # + # @param subject [String] the unsupported call, e.g. +"Ably::Rest::Client.new"+ + # @param factory [String] the supported call to use instead + # @return [Ably::Exceptions::DirectConstructionNotSupported] + # @api private + def direct_construction_error(subject, factory) + Ably::Exceptions::DirectConstructionNotSupported.new <<~MSG + #{subject} is not supported: Ably clients cannot be constructed directly. + + Use the factory function of the Ably package you installed, for example: + #{factory} # ably-pubsub-server + + The package a client is created from is what declares the client's side to the + platform. A directly constructed client declares none, so the platform cannot + classify it for MAU billing. + MSG + end + + private + + # +allocate+ skips the class's overridden +new+, so +initialize+ has to be invoked + # explicitly. Every client instance in the library is built through here. + def construct(klass, options) + klass.allocate.tap { |client| client.send(:initialize, options) } + end + end + end +end diff --git a/core/lib/ably/realtime.rb b/core/lib/ably/realtime.rb index dc9766628..f6171e1f1 100644 --- a/core/lib/ably/realtime.rb +++ b/core/lib/ably/realtime.rb @@ -28,10 +28,10 @@ require 'ably/realtime/client/outgoing_message_dispatcher' module Ably - # Realtime provides the top-level class to be instanced for the Ably Realtime library + # Realtime is the namespace of the stateful realtime client and the models it returns. # # @example - # client = Ably::Realtime.new("xxxxx") + # client = Ably::PubSub::Server.create_realtime_client("xxxxx") # channel = client.channel("test") # channel.subscribe do |message| # message[:name] #=> "greeting" @@ -39,22 +39,18 @@ module Ably # channel.publish "greeting", "data" # module Realtime - # Convenience method providing an alias to {Ably::Realtime::Client} constructor. + # Refuses construction. This was a convenience alias for the {Ably::Realtime::Client} + # constructor, which no longer accepts direct construction: the package a client is + # created from is what declares the client's side to the platform, and a client + # constructed here declares none. # - # @param (see Ably::Realtime::Client#initialize) - # @option options (see Ably::Realtime::Client#initialize) + # Use {Ably::PubSub::Server.create_realtime_client} from the +ably-pubsub-server+ gem. # - # @return [Ably::Realtime::Client] - # - # @example - # # create a new client authenticating with basic auth - # client = Ably::Realtime.new('key.id:secret') - # - # # create a new client authenticating with basic auth and a client_id - # client = Ably::Realtime.new(key: 'key.id:secret', client_id: 'john') - # - def self.new(options) - Ably::Realtime::Client.new(options) + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::Realtime.new', 'Ably::PubSub::Server.create_realtime_client(options)' + ) end end end diff --git a/core/lib/ably/realtime/auth.rb b/core/lib/ably/realtime/auth.rb index c1fc59831..20cb68f09 100644 --- a/core/lib/ably/realtime/auth.rb +++ b/core/lib/ably/realtime/auth.rb @@ -64,7 +64,7 @@ def initialize(client) # # @example # # will issue a simple token request using basic auth - # client = Ably::Rest::Client.new(key: 'key.id:secret') + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') # client.auth.authorize do |token_details| # token_details #=> Ably::Models::TokenDetails # end @@ -160,7 +160,7 @@ def authorise_sync(*args) # # @example # # simple token request using basic auth - # client = Ably::Rest::Client.new(key: 'key.id:secret') + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') # client.auth.request_token do |token_details| # token_details #=> Ably::Models::TokenDetails # end diff --git a/core/lib/ably/realtime/client.rb b/core/lib/ably/realtime/client.rb index b35aaa94b..57d943d02 100644 --- a/core/lib/ably/realtime/client.rb +++ b/core/lib/ably/realtime/client.rb @@ -77,6 +77,17 @@ class Client def_delegators :@rest_client, :log_level def_delegators :@rest_client, :options + # Refuses direct construction: the package a client is created from is what declares + # the client's side to the platform, and a directly constructed client declares none. + # Use {Ably::PubSub::Server.create_realtime_client} from the +ably-pubsub-server+ gem. + # + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::Realtime::Client.new', 'Ably::PubSub::Server.create_realtime_client(options)' + ) + end + # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection. # # @spec RSC1 @@ -100,11 +111,14 @@ class Client # @return [Ably::Realtime::Client] # # @example - # # Constructs a {Ably::Realtime::Client} object using an Ably API key or token string. - # client = Ably::Realtime::Client.new('key.id:secret') + # # Constructs a client using an Ably API key or token string. + # client = Ably::PubSub::Server.create_realtime_client('key.id:secret') + # + # # Constructs a client using an Ably options object. + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret', client_id: 'john') # - # # Constructs a {Ably::Realtime::Client} object using an Ably options object. - # client = Ably::Realtime::Client.new(key: 'key.id:secret', client_id: 'john') + # Applications do not call this constructor: {.new} refuses direct construction, and + # a per-side package reaches it through {Ably::Internal.create_realtime_client}. # def initialize(options) raise ArgumentError, 'Options Hash is expected' if options.nil? @@ -121,7 +135,7 @@ def initialize(options) @transport_params = options.delete(:transport_params).to_h.each_with_object({}) do |(key, value), acc| acc[key.to_s] = value.to_s end - @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self)) + @rest_client = Ably::Internal.create_rest_client(options.merge(realtime_client: self)) @echo_messages = rest_client.options.fetch_with_default(:echo_messages, true) @queue_messages = rest_client.options.fetch_with_default(:queue_messages, true) @custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host] diff --git a/core/lib/ably/realtime/connection.rb b/core/lib/ably/realtime/connection.rb index 45e6abe5c..1f1affc98 100644 --- a/core/lib/ably/realtime/connection.rb +++ b/core/lib/ably/realtime/connection.rb @@ -246,7 +246,7 @@ def connect(&success_block) # If the ping is not received within an acceptable timeframe, the block will be called with +nil+ as he first argument # # @example - # client = Ably::Rest::Client.new(key: 'key.id:secret') + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') # client.connection.ping do |elapsed_s| # puts "Ping took #{elapsed_s}s" # end diff --git a/core/lib/ably/rest.rb b/core/lib/ably/rest.rb index 28c9d130a..eca5d41d4 100644 --- a/core/lib/ably/rest.rb +++ b/core/lib/ably/rest.rb @@ -11,30 +11,26 @@ end module Ably - # Rest provides the top-level class to be instanced for the Ably Rest library + # Rest is the namespace of the stateless HTTP client and the models it returns. # # @example - # client = Ably::Rest.new("xxxxx") + # client = Ably::PubSub::Server.create_http_client("xxxxx") # channel = client.channel("test") # channel.publish "greeting", "data" # module Rest - # Convenience method providing an alias to {Ably::Rest::Client} constructor. + # Refuses construction. This was a convenience alias for the {Ably::Rest::Client} + # constructor, which no longer accepts direct construction: the package a client is + # created from is what declares the client's side to the platform, and a client + # constructed here declares none. # - # @param (see Ably::Rest::Client#initialize) - # @option options (see Ably::Rest::Client#initialize) + # Use {Ably::PubSub::Server.create_http_client} from the +ably-pubsub-server+ gem. # - # @return [Ably::Rest::Client] - # - # @example - # # create a new client authenticating with basic auth - # client = Ably::Rest.new('key.id:secret') - # - # # create a new client authenticating with basic auth and a client_id - # client = Ably::Rest.new(key: 'key.id:secret', client_id: 'john') - # - def self.new(options) - Ably::Rest::Client.new(options) + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::Rest.new', 'Ably::PubSub::Server.create_http_client(options)' + ) end end end diff --git a/core/lib/ably/rest/client.rb b/core/lib/ably/rest/client.rb index 04e60a5be..b7404d00d 100644 --- a/core/lib/ably/rest/client.rb +++ b/core/lib/ably/rest/client.rb @@ -125,6 +125,17 @@ class Client # @return [Integer] attr_reader :max_frame_size + # Refuses direct construction: the package a client is created from is what declares + # the client's side to the platform, and a directly constructed client declares none. + # Use {Ably::PubSub::Server.create_http_client} from the +ably-pubsub-server+ gem. + # + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::Rest::Client.new', 'Ably::PubSub::Server.create_http_client(options)' + ) + end + # Constructs a {Ably::Rest::Client} object using an Ably API key or token string. # # @spec RSC1 @@ -168,10 +179,13 @@ class Client # # @example # # Create a new client authenticating with basic auth using a String object - # client = Ably::Rest::Client.new('key.id:secret') + # client = Ably::PubSub::Server.create_http_client('key.id:secret') + # + # # Construct a client using a Hash object. + # client = Ably::PubSub::Server.create_http_client(key: 'key.id:secret', client_id: 'john') # - # # Construct a RestClient object using a Hash object. - # client = Ably::Rest::Client.new(key: 'key.id:secret', client_id: 'john') + # Applications do not call this constructor: {.new} refuses direct construction, and + # a per-side package reaches it through {Ably::Internal.create_rest_client}. # def initialize(options) raise ArgumentError, 'Options Hash is expected' if options.nil? diff --git a/server/README.md b/server/README.md index da5eab247..eecd738f6 100644 --- a/server/README.md +++ b/server/README.md @@ -13,7 +13,10 @@ gem 'ably-pubsub-server' ## Usage -The factory functions are the only recommended entry points: +The factory functions are the only entry points. The client classes refuse direct +construction — `Ably::Rest::Client.new` and `Ably::Realtime::Client.new` raise +`Ably::Exceptions::DirectConstructionNotSupported` — because the package a client is +created from is what declares its side, and a directly constructed client declares none. ```ruby require 'ably/pubsub/server' diff --git a/server/lib/ably/pubsub/server.rb b/server/lib/ably/pubsub/server.rb index 436546cb1..ed87bab5f 100644 --- a/server/lib/ably/pubsub/server.rb +++ b/server/lib/ably/pubsub/server.rb @@ -3,8 +3,9 @@ module Ably module PubSub - # The Ably Pub/Sub SDK for servers. The factory functions here are the only - # recommended entry points of the +ably-pubsub-server+ gem. + # The Ably Pub/Sub SDK for servers. The factory functions here are the only entry + # points of the +ably-pubsub-server+ gem: the client classes refuse direct construction + # (see {Ably::Internal}), so a client always carries the side its package declares. module Server # The agent identifier declaring the server side. # @@ -29,7 +30,7 @@ class << self # # @return [Ably::Rest::Client] def create_http_client(options) - Ably::Rest::Client.new(options_with_side_agent(options)) + Ably::Internal.create_rest_client(options_with_side_agent(options)) end # Creates a stateful realtime client declaring the server side. @@ -39,7 +40,7 @@ def create_http_client(options) # # @return [Ably::Realtime::Client] def create_realtime_client(options) - Ably::Realtime::Client.new(options_with_side_agent(options)) + Ably::Internal.create_realtime_client(options_with_side_agent(options)) end private diff --git a/spec/acceptance/realtime/auth_spec.rb b/spec/acceptance/realtime/auth_spec.rb index 435070bff..580bfac90 100644 --- a/spec/acceptance/realtime/auth_spec.rb +++ b/spec/acceptance/realtime/auth_spec.rb @@ -16,7 +16,7 @@ def disconnect_transport(connection) vary_by_protocol do let(:default_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_options } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:auth) { client.auth } context 'with basic auth' do @@ -200,7 +200,7 @@ def disconnect_transport(connection) end context 'with auth_callback blocking' do - let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) } + let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } let(:client_options) { default_options.merge(auth_callback: auth_callback) } let(:pause) { 5 } @@ -231,7 +231,7 @@ def disconnect_transport(connection) context 'when implicitly called, with an explicit ClientOptions client_id' do let(:client_id) { random_str } let(:client_options) { default_options.merge(auth_callback: lambda { |token_params| auth_token_object }, client_id: client_id, log_level: :none) } - let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')) } + let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: 'invalid')) } context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do let(:auth_token_object) { rest_auth_client.auth.request_token } @@ -280,8 +280,8 @@ def disconnect_transport(connection) let(:client_id) { random_str } let(:client_options) { default_options.merge(auth_callback: auth_proc, client_id: client_id, log_level: :none) } - let(:valid_auth_token) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: client_id)).auth.request_token } - let(:invalid_auth_token) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')).auth.request_token } + let(:valid_auth_token) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: client_id)).auth.request_token } + let(:invalid_auth_token) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: 'invalid')).auth.request_token } context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do it 'rejects a TokenDetails object with an incompatible client_id and fails with an exception' do @@ -301,8 +301,8 @@ def disconnect_transport(connection) end context 'when already authenticated with a valid token' do - let(:rest_client) { Ably::Rest::Client.new(default_options) } - let(:client_publisher) { auto_close Ably::Realtime::Client.new(default_options) } + let(:rest_client) { Ably::Internal.create_rest_client(default_options) } + let(:client_publisher) { auto_close Ably::Internal.create_realtime_client(default_options) } let(:basic_capability) { JSON.dump("foo" => ["subscribe"]) } let(:basic_token_cb) { lambda do |token_params| rest_client.auth.create_token_request({ capability: basic_capability }) @@ -833,7 +833,7 @@ def disconnect_transport(connection) end describe '#client_id_validated?' do - let(:auth) { Ably::Rest::Client.new(default_options.merge(key: api_key)).auth } + let(:auth) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)).auth } context 'when using basic auth' do let(:client_options) { default_options.merge(key: api_key) } @@ -1082,7 +1082,7 @@ def disconnect_transport(connection) context 'when using auth_callback' do let(:token_callback) do lambda do |token_params| - Ably::Rest::Client.new(default_options).auth.request_token({}, { auth_url: auth_url, auth_params: auth_params }).token + Ably::Internal.create_rest_client(default_options).auth.request_token({}, { auth_url: auth_url, auth_params: auth_params }).token end end let(:client_options) { default_options.merge(auth_callback: token_callback) } diff --git a/spec/acceptance/realtime/channel_history_spec.rb b/spec/acceptance/realtime/channel_history_spec.rb index 56f1b4a00..99ed28ec9 100644 --- a/spec/acceptance/realtime/channel_history_spec.rb +++ b/spec/acceptance/realtime/channel_history_spec.rb @@ -5,11 +5,11 @@ vary_by_protocol do let(:default_options) { options.merge(key: api_key, environment: environment, protocol: protocol) } - let(:client) { auto_close Ably::Realtime::Client.new(default_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(default_options) } let(:channel) { client.channel(channel_name) } let(:rest_channel) { client.rest_client.channel(channel_name) } - let(:client2) { auto_close Ably::Realtime::Client.new(default_options) } + let(:client2) { auto_close Ably::Internal.create_realtime_client(default_options) } let(:channel2) { client2.channel(channel_name) } let(:channel_name) { "persisted:#{random_str(2)}" } diff --git a/spec/acceptance/realtime/channel_spec.rb b/spec/acceptance/realtime/channel_spec.rb index 25f4a7c66..e0e766739 100644 --- a/spec/acceptance/realtime/channel_spec.rb +++ b/spec/acceptance/realtime/channel_spec.rb @@ -6,14 +6,14 @@ let(:default_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_options } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:connection) { client.connection } let(:channel_name) { random_str } let(:payload) { random_str } let(:channel) { client.channel(channel_name) } let(:messages) { [] } - let(:sub_client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:sub_client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:sub_channel) { sub_client.channel(channel_name) } def disconnect_transport @@ -23,7 +23,7 @@ def disconnect_transport describe 'initialization' do context 'with :auto_connect option set to false on connection' do let(:client) do - auto_close Ably::Realtime::Client.new(default_options.merge(auto_connect: false)) + auto_close Ably::Internal.create_realtime_client(default_options.merge(auto_connect: false)) end it 'remains initialized when accessing a channel' do @@ -358,7 +358,7 @@ def disconnect_transport it 'attaches all channels', em_timeout: 15 do connection_count.times.map do - auto_close Ably::Realtime::Client.new(default_options) + auto_close Ably::Internal.create_realtime_client(default_options) end.each do |client| channel_count.times.map do |index| client.channel("channel-#{index}").attach do @@ -383,7 +383,7 @@ def disconnect_transport ) end let(:restricted_client) do - auto_close Ably::Realtime::Client.new(auth_options) + auto_close Ably::Internal.create_realtime_client(auth_options) end let(:restricted_channel) { restricted_client.channel("cansubscribe:foo") } @@ -1377,7 +1377,7 @@ def disconnect_transport end context 'with two invalid message out of 12' do - let(:rest_client) { Ably::Rest::Client.new(default_options.merge(client_id: 'valid')) } + let(:rest_client) { Ably::Internal.create_rest_client(default_options.merge(client_id: 'valid')) } let(:invalid_messages) do 2.times.map do |index| @@ -1416,7 +1416,7 @@ def disconnect_transport end context 'only invalid messages' do - let(:rest_client) { Ably::Rest::Client.new(default_options.merge(client_id: 'valid')) } + let(:rest_client) { Ably::Internal.create_rest_client(default_options.merge(client_id: 'valid')) } let(:invalid_messages) do 10.times.map do |index| @@ -1462,7 +1462,7 @@ def disconnect_transport it 'publishes all messages, all success callbacks are called, and a history request confirms all messages were published' do connection_count.times.map do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end.each do |client| channel = client.channels.get(channel_name) messages.each do |message| @@ -1497,9 +1497,9 @@ def disconnect_transport context 'identified clients' do context 'when authenticated with a wildcard client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: '*') } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: '*') } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a valid client_id in the message' do @@ -1546,9 +1546,9 @@ def disconnect_transport end context 'when authenticated with a Token string with an implicit client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: 'valid').token } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid').token } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'before the client is CONNECTED and the client\'s identity has been obtained' do @@ -1637,9 +1637,9 @@ def disconnect_transport end context 'when authenticated with a valid client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: 'valid') } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid') } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a valid client_id' do @@ -1686,9 +1686,9 @@ def disconnect_transport end context 'when anonymous and no client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: nil) } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: nil) } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a client_id in the message' do @@ -1722,7 +1722,7 @@ def disconnect_transport end context 'message size exceeded (#TO3l8)' do - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'and max_message_size is default (65536 bytes)' do @@ -2569,7 +2569,7 @@ def self.build_flags(flags) connection_id = client.connection.id expect(channel_state_change.resumed).to be_falsey - recover_client = auto_close Ably::Realtime::Client.new(client_options.merge(recover: client.connection.create_recovery_key)) + recover_client = auto_close Ably::Internal.create_realtime_client(client_options.merge(recover: client.connection.create_recovery_key)) recover_client.connection.once(:connected) do expect(recover_client.connection.id).to eql(connection_id) recover_channel = recover_client.channels.get(channel_name) @@ -2586,7 +2586,7 @@ def self.build_flags(flags) client.connection.once(:connected) do recovery_key = client.connection.create_recovery_key client.connection.once(:closed) do - recover_client = auto_close Ably::Realtime::Client.new(client_options.merge(recover: recovery_key, log_level: :error)) + recover_client = auto_close Ably::Internal.create_realtime_client(client_options.merge(recover: recovery_key, log_level: :error)) recover_client.connection.once(:connected) do recover_channel = recover_client.channels.get(channel_name) recover_channel.attach @@ -2646,7 +2646,7 @@ def self.build_flags(flags) %w(suspended failed).each do |channel_state| context(channel_state) do let(:client) do - auto_close Ably::Realtime::Client.new(default_options.merge(log_level: :error)) + auto_close Ably::Internal.create_realtime_client(default_options.merge(log_level: :error)) end specify 'all queued messages fail with NACK (#RTL11)' do diff --git a/spec/acceptance/realtime/channels_spec.rb b/spec/acceptance/realtime/channels_spec.rb index 9d385da7c..e8958f0be 100644 --- a/spec/acceptance/realtime/channels_spec.rb +++ b/spec/acceptance/realtime/channels_spec.rb @@ -21,7 +21,7 @@ { key: api_key, environment: environment, protocol: protocol } end let(:client) do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end let(:channel_name) { random_str } let(:options) do diff --git a/spec/acceptance/realtime/client_spec.rb b/spec/acceptance/realtime/client_spec.rb index 131ab8f41..cdef09a9a 100644 --- a/spec/acceptance/realtime/client_spec.rb +++ b/spec/acceptance/realtime/client_spec.rb @@ -11,8 +11,8 @@ let(:connection) { subject.connection } let(:auth_params) { subject.auth.auth_params_sync } - subject { auto_close Ably::Realtime::Client.new(client_options) } - let(:sub_client) { auto_close Ably::Realtime::Client.new(client_options) } + subject { auto_close Ably::Internal.create_realtime_client(client_options) } + let(:sub_client) { auto_close Ably::Internal.create_realtime_client(client_options) } context 'initialization' do context 'basic auth' do @@ -27,7 +27,7 @@ context 'with an invalid API key' do let(:custom_logger_object) { TestLogger.new } - let(:client) { Ably::Realtime::Client.new(client_options.merge(key: 'app.key:secret', logger: custom_logger_object)) } + let(:client) { Ably::Internal.create_realtime_client(client_options.merge(key: 'app.key:secret', logger: custom_logger_object)) } it 'logs an entry with a help href url matching the code #TI5' do client.connect @@ -61,7 +61,7 @@ [true, false].each do |tls_enabled| context "with TLS #{tls_enabled ? 'enabled' : 'disabled'}" do let(:capability) { { :foo => ["publish"] } } - let(:token_client) { auto_close Ably::Realtime::Client.new(default_options) } + let(:token_client) { auto_close Ably::Internal.create_realtime_client(default_options) } let(:token_details) { token_client.auth.request_token_sync(capability: capability) } let(:client_options) { default_options.merge(token: token_details.token) } @@ -95,7 +95,7 @@ let(:auth) { subject.auth } subject do - auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: Proc.new do + auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: Proc.new do @block_called = true auth.create_token_request_sync(client_id: client_id) end)) @@ -134,9 +134,9 @@ end context 'with a wildcard client_id token ' do - subject { auto_close Ably::Realtime::Client.new(client_options) } + subject { auto_close Ably::Internal.create_realtime_client(client_options) } let(:client_options) { default_options.merge(auth_callback: lambda { |token_params| auth_token_object }, client_id: client_id) } - let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) } + let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } let(:auth_token_object) { rest_auth_client.auth.request_token(client_id: '*') } context 'and an explicit client_id in ClientOptions' do @@ -175,7 +175,7 @@ context 'with an invalid wildcard "*" :client_id' do it 'raises an exception' do - expect { Ably::Realtime::Client.new(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError + expect { Ably::Internal.create_realtime_client(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError stop_reactor end end @@ -442,7 +442,7 @@ context 'queue_messages ClientOption' do context 'when true' do - subject { auto_close Ably::Realtime::Client.new(client_options.merge(auto_connect: false)) } + subject { auto_close Ably::Internal.create_realtime_client(client_options.merge(auto_connect: false)) } it 'will queue messages whilst connecting and publish once connected' do sub_channel.attach do @@ -459,7 +459,7 @@ end context 'when false' do - subject { auto_close Ably::Realtime::Client.new(client_options.merge(auto_connect: false, queue_messages: false)) } + subject { auto_close Ably::Internal.create_realtime_client(client_options.merge(auto_connect: false, queue_messages: false)) } it 'will reject messages on an initializing connection' do sub_channel.attach do diff --git a/spec/acceptance/realtime/connection_failures_spec.rb b/spec/acceptance/realtime/connection_failures_spec.rb index 64a1225bb..560147665 100644 --- a/spec/acceptance/realtime/connection_failures_spec.rb +++ b/spec/acceptance/realtime/connection_failures_spec.rb @@ -11,10 +11,10 @@ let(:client_options) { default_options } let(:client) do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end let(:rest_client) do - Ably::Rest::Client.new(default_options) + Ably::Internal.create_rest_client(default_options) end context 'authentication failure' do @@ -151,7 +151,7 @@ # Timeout +5 seconds, beyond default allowed timeout before do - token_response = Ably::Rest::Client.new(default_options).auth.request_token + token_response = Ably::Internal.create_rest_client(default_options).auth.request_token WebMock.enable! stub_request(:get, auth_url). @@ -559,7 +559,7 @@ def time_passed let(:channel_name) { random_str } let(:channel) { client.channel(channel_name) } let(:publishing_client) do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end let(:publishing_client_channel) { publishing_client.channel(channel_name) } let(:client_options) { default_options.merge(log_level: :none) } diff --git a/spec/acceptance/realtime/connection_spec.rb b/spec/acceptance/realtime/connection_spec.rb index d23b0cadb..57794de2d 100644 --- a/spec/acceptance/realtime/connection_spec.rb +++ b/spec/acceptance/realtime/connection_spec.rb @@ -11,7 +11,7 @@ end let(:client_options) { default_options } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } before(:example) do EventMachine.add_shutdown_hook do @@ -36,7 +36,7 @@ context 'with :auto_connect option set to false' do let(:client) do - auto_close Ably::Realtime::Client.new(default_options.merge(auto_connect: false)) + auto_close Ably::Internal.create_realtime_client(default_options.merge(auto_connect: false)) end it 'does not connect automatically' do @@ -129,7 +129,7 @@ let(:token_callback) do lambda do |token_params| auth_requests << Time.now - Ably::Rest::Client.new(default_options).auth.request_token(ttl: ttl).token + Ably::Internal.create_rest_client(default_options).auth.request_token(ttl: ttl).token end end let(:client_options) { default_options.merge(auth_callback: token_callback, fallback_hosts: []) } @@ -245,11 +245,11 @@ lambda do |token_params| sleep 2 auth_requests << Time.now - Ably::Rest::Client.new(default_options).auth.request_token(ttl: ttl).token + Ably::Internal.create_rest_client(default_options).auth.request_token(ttl: ttl).token end end let(:client_options) { default_options.merge(auth_callback: token_callback) } - let(:publishing_client) { auto_close Ably::Realtime::Client.new(default_options) } + let(:publishing_client) { auto_close Ably::Internal.create_realtime_client(default_options) } let(:publishing_channel) { publishing_client.channels.get(channel_name) } let(:messages_received) { [] } @@ -299,7 +299,7 @@ def publish_and_check_disconnect(options = {}) "#{app_id}.invalid-token-invalid-token-invalid-token" else @token_issued = true - Ably::Rest::Client.new(default_options).auth.request_token(ttl: ttl).token + Ably::Internal.create_rest_client(default_options).auth.request_token(ttl: ttl).token end end end @@ -327,7 +327,7 @@ def publish_and_check_disconnect(options = {}) let!(:expired_token_details) do # Request a token synchronously - token_client = auto_close Ably::Realtime::Client.new(default_options) + token_client = auto_close Ably::Internal.create_realtime_client(default_options) token_client.auth.request_token_sync(ttl: ttl) end @@ -365,7 +365,7 @@ def publish_and_check_disconnect(options = {}) context 'with opaque token string that contain an implicit client_id' do let(:client_options) { default_options.merge(token: token_string, key: nil) } - let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) } + let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } let(:token_string) { rest_auth_client.auth.request_token(client_id: client_id).token } context 'string' do @@ -662,7 +662,7 @@ def expect_ordered_phases end describe 'once connected' do - let(:client2) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client2) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:connection2) { client2.connection } describe 'connection#id' do @@ -749,7 +749,7 @@ def expect_ordered_phases let(:batches) { 6 } let(:messages_per_batch) { 10 } - let(:publishing_client) { auto_close Ably::Realtime::Client.new(default_options) } + let(:publishing_client) { auto_close Ably::Internal.create_realtime_client(default_options) } let(:channel_name) { random_str } let(:publishing_channel) { publishing_client.channels.get(channel_name) } let(:receiving_channel) { client.channels.get(channel_name) } @@ -1327,7 +1327,7 @@ def log_connection_changes let(:channel_name) { random_str } let(:channel) { client.channel(channel_name) } let(:publishing_client) do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end let(:publishing_client_channel) { publishing_client.channel(channel_name) } @@ -1402,7 +1402,7 @@ def self.available_states end connection.once(:failed) do - recover_client = auto_close Ably::Realtime::Client.new(default_options.merge(recover: recovery_key)) + recover_client = auto_close Ably::Internal.create_realtime_client(default_options.merge(recover: recovery_key)) recover_client.connection.on(:connected) do expect(recover_client.connection.id).to eql(previous_connection_id) stop_reactor @@ -1426,7 +1426,7 @@ def self.available_states end connection.on(:failed) do - recover_client = auto_close Ably::Realtime::Client.new(default_options.merge(recover: recovery_key)) + recover_client = auto_close Ably::Internal.create_realtime_client(default_options.merge(recover: recovery_key)) recover_client_channel = recover_client.channel(channel_name) recover_client_channel.attach do expect(recover_client.connection.id).to eql(connection_id) @@ -1458,7 +1458,7 @@ def self.available_states end connection.on(:failed) do - recover_client = auto_close Ably::Realtime::Client.new(default_options.merge(recover: recovery_key)) + recover_client = auto_close Ably::Internal.create_realtime_client(default_options.merge(recover: recovery_key)) recover_client_channel = recover_client.channel(channel_name) recover_client_channel.attach do expect(recover_client.connection.id).to eql(connection_id) @@ -1490,7 +1490,7 @@ def self.available_states end connection.on(:failed) do - recover_client = auto_close Ably::Realtime::Client.new(default_options.merge(recover: recovery_key)) + recover_client = auto_close Ably::Internal.create_realtime_client(default_options.merge(recover: recovery_key)) recover_client_channel = recover_client.channel(channel_name) expect(recover_client.connection.send(:client_msg_serial)).to eql(msg_serial) @@ -1555,7 +1555,7 @@ def self.available_states it 'opens each with a unique connection#id and connection#key' do connection_count.times.map do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end.each do |client| client.connection.on(:connected) do connection_ids << client.connection.id diff --git a/spec/acceptance/realtime/message_spec.rb b/spec/acceptance/realtime/message_spec.rb index 655ae93cf..8220e3b64 100644 --- a/spec/acceptance/realtime/message_spec.rb +++ b/spec/acceptance/realtime/message_spec.rb @@ -9,12 +9,12 @@ let(:default_options) { options.merge(key: api_key, environment: environment, protocol: protocol) } let(:client_options) { default_options } let(:client) do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end let(:channel) { client.channel(channel_name) } let(:other_client) do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end let(:other_client_channel) { other_client.channel(channel_name) } @@ -293,12 +293,12 @@ def publish_and_check_extras(extras) context 'with :echo_messages option set to false' do let(:no_echo_client) do - auto_close Ably::Realtime::Client.new(default_options.merge(echo_messages: false)) + auto_close Ably::Internal.create_realtime_client(default_options.merge(echo_messages: false)) end let(:no_echo_channel) { no_echo_client.channel(channel_name) } let(:rest_client) do - Ably::Rest::Client.new(default_options) + Ably::Internal.create_rest_client(default_options) end it 'will not echo messages to the client but will still broadcast messages to other connected clients', em_timeout: 10 do @@ -398,7 +398,7 @@ def publish_and_check_extras(extras) context 'without suitable publishing permissions' do let(:restricted_client) do - auto_close Ably::Realtime::Client.new(options.merge(key: restricted_api_key, environment: environment, protocol: protocol, :log_level => :error)) + auto_close Ably::Internal.create_realtime_client(options.merge(key: restricted_api_key, environment: environment, protocol: protocol, :log_level => :error)) end let(:restricted_channel) { restricted_client.channel("cansubscribe:example") } let(:payload) { 'Test message without permission to publish' } @@ -550,7 +550,7 @@ def publish_and_check_extras(extras) context 'subscribing with a different transport protocol' do let(:other_protocol) { protocol == :msgpack ? :json : :msgpack } let(:other_client) do - auto_close Ably::Realtime::Client.new(default_options.merge(protocol: other_protocol)) + auto_close Ably::Internal.create_realtime_client(default_options.merge(protocol: other_protocol)) end let(:cipher_options) { { key: Ably::Util::Crypto.generate_random_key, algorithm: 'aes', mode: 'cbc', key_length: 256 } } @@ -827,10 +827,10 @@ def publish_and_check_extras(extras) context 'over a JSON transport' do let(:realtime_client) do - auto_close Ably::Realtime::Client.new(client_options) + auto_close Ably::Internal.create_realtime_client(client_options) end let(:rest_client) do - Ably::Rest::Client.new(client_options) + Ably::Internal.create_rest_client(client_options) end let(:realtime_channel) { realtime_client.channels.get(channel_name) } @@ -881,10 +881,10 @@ def publish_and_check_extras(extras) JSON.parse(File.read(fixtures_path))['messages'].each do |encoding_spec| context "when publishing a #{encoding_spec['expectedType']} using JSON protocol" do let(:rest_publish_client) do - Ably::Rest::Client.new(client_options.merge(protocol: :json)) + Ably::Internal.create_rest_client(client_options.merge(protocol: :json)) end let(:realtime_subscribe_client) do - Ably::Realtime::Client.new(client_options.merge(protocol: :msgpack)) + Ably::Internal.create_realtime_client(client_options.merge(protocol: :msgpack)) end let(:realtime_subscribe_channel) { realtime_subscribe_client.channels.get(channel_name) } @@ -909,10 +909,10 @@ def publish_and_check_extras(extras) context "when retrieving a #{encoding_spec['expectedType']} using JSON protocol" do let(:rest_publish_client) do - Ably::Rest::Client.new(client_options.merge(protocol: :msgpack)) + Ably::Internal.create_rest_client(client_options.merge(protocol: :msgpack)) end let(:rest_retrieve_client) do - Ably::Rest::Client.new(client_options.merge(protocol: :json)) + Ably::Internal.create_rest_client(client_options.merge(protocol: :json)) end let(:rest_publish_channel) { rest_publish_client.channels.get(channel_name) } diff --git a/spec/acceptance/realtime/presence_history_spec.rb b/spec/acceptance/realtime/presence_history_spec.rb index cdd4cc2aa..0f6cbb51f 100644 --- a/spec/acceptance/realtime/presence_history_spec.rb +++ b/spec/acceptance/realtime/presence_history_spec.rb @@ -7,11 +7,11 @@ let(:channel_name) { "persisted:#{random_str(2)}" } - let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(client_id: random_str)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(default_options.merge(client_id: random_str)) } let(:channel_client_one) { client_one.channel(channel_name) } let(:presence_client_one) { channel_client_one.presence } - let(:client_two) { auto_close Ably::Realtime::Client.new(default_options.merge(client_id: random_str)) } + let(:client_two) { auto_close Ably::Internal.create_realtime_client(default_options.merge(client_id: random_str)) } let(:channel_client_two) { client_two.channel(channel_name) } let(:presence_client_two) { channel_client_two.presence } diff --git a/spec/acceptance/realtime/presence_spec.rb b/spec/acceptance/realtime/presence_spec.rb index c24f5cee0..a7fbacbf5 100644 --- a/spec/acceptance/realtime/presence_spec.rb +++ b/spec/acceptance/realtime/presence_spec.rb @@ -8,13 +8,13 @@ let(:default_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_options } - let(:anonymous_client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:anonymous_client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:client_one_id) { random_str } - let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(client_id: client_one_id)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(client_id: client_one_id)) } let(:client_two_id) { random_str } - let(:client_two) { auto_close Ably::Realtime::Client.new(client_options.merge(client_id: client_two_id)) } + let(:client_two) { auto_close Ably::Internal.create_realtime_client(client_options.merge(client_id: client_two_id)) } - let(:wildcard_token) { lambda { |token_params| Ably::Rest::Client.new(client_options).auth.request_token(client_id: '*') } } + let(:wildcard_token) { lambda { |token_params| Ably::Internal.create_rest_client(client_options).auth.request_token(client_id: '*') } } let(:channel_name) { "presence-#{random_str(4)}" } let(:channel_anonymous_client) { anonymous_client.channel(channel_name) } let(:presence_anonymous_client) { channel_anonymous_client.presence } @@ -136,7 +136,7 @@ def setup_test(method_name, args, options) end context 'when :queue_messages client option is false' do - let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(queue_messages: false, client_id: client_id)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(default_options.merge(queue_messages: false, client_id: client_id)) } context 'and connection state initialized' do it 'fails the deferrable' do @@ -162,7 +162,7 @@ def setup_test(method_name, args, options) end context 'and connection state disconnected' do - let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(queue_messages: false, client_id: client_id, :log_level => :error)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(default_options.merge(queue_messages: false, client_id: client_id, :log_level => :error)) } it 'fails the deferrable' do client_one.connection.once(:connected) do @@ -353,9 +353,9 @@ def presence_action(method_name, data) shared_examples_for 'a presence on behalf of another client method' do |method_name| context ":#{method_name} when authenticated with a wildcard client_id" do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: '*').token } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: '*').token } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { auto_close Ably::Realtime::Client.new(client_options) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:presence_channel) { client.channels.get(channel_name).presence } context 'and a valid client_id' do @@ -391,9 +391,9 @@ def presence_action(method_name, data) end context ":#{method_name} when authenticated with a valid client_id" do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: 'valid').token } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid').token } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { auto_close Ably::Realtime::Client.new(client_options.merge(log_level: :error)) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options.merge(log_level: :error)) } let(:channel) { client.channels.get(channel_name) } let(:presence_channel) { channel.presence } @@ -433,9 +433,9 @@ def presence_action(method_name, data) end context ":#{method_name} when anonymous and no client_id" do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: nil).token } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: nil).token } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { auto_close Ably::Realtime::Client.new(client_options.merge(log_level: :error)) } + let(:client) { auto_close Ably::Internal.create_realtime_client(client_options.merge(log_level: :error)) } let(:channel) { client.channels.get(channel_name) } let(:presence_channel) { channel.presence } @@ -605,7 +605,7 @@ def presence_action(method_name, data) let(:leave_action) { 3 } let(:now) { Time.now.to_i * 1000 } let(:entered) { [] } - let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(auth_callback: wildcard_token)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(default_options.merge(auth_callback: wildcard_token)) } def setup_members_on(presence) enter_expected_count.times do |indx| @@ -768,7 +768,7 @@ def allow_sync_fabricate_data_final_sync_and_assert_members let(:present) { [] } let(:entered) { [] } let(:sync_pages_received) { [] } - let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: wildcard_token)) } def setup_members_on(presence) enter_expected_count.times do |indx| @@ -1092,7 +1092,7 @@ def setup_members_on(presence) context 'without necessary capabilities to join presence' do let(:restricted_client) do - auto_close Ably::Realtime::Client.new(default_options.merge(key: restricted_api_key, log_level: :fatal)) + auto_close Ably::Internal.create_realtime_client(default_options.merge(key: restricted_api_key, log_level: :fatal)) end let(:restricted_channel) { restricted_client.channel("cansubscribe:channel") } let(:restricted_presence) { restricted_channel.presence } @@ -1292,8 +1292,8 @@ def setup_members_on(presence) let(:client_count) { 5 } let(:clients) { [] } let(:data) { random_str } - let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) } - let(:client_two) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: wildcard_token)) } + let(:client_two) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: wildcard_token)) } context '#enter_client' do context 'multiple times on the same channel with different client_ids' do @@ -1347,7 +1347,7 @@ def setup_members_on(presence) context 'without necessary capabilities to enter on behalf of another client' do let(:restricted_client) do - auto_close Ably::Realtime::Client.new(default_options.merge(key: restricted_api_key, log_level: :fatal)) + auto_close Ably::Internal.create_realtime_client(default_options.merge(key: restricted_api_key, log_level: :fatal)) end let(:restricted_channel) { restricted_client.channel("cansubscribe:channel") } let(:restricted_presence) { restricted_channel.presence } @@ -1632,7 +1632,7 @@ def setup_members_on(presence) let(:pages) { 2 } let(:members_per_page) { 100 } let(:sync_pages_received) { [] } - let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: wildcard_token)) } let(:client_options) { default_options.merge(log_level: :none) } def connect_members_deferrables @@ -1815,8 +1815,8 @@ def connect_members_deferrables end context 'with lots of members on different clients' do - let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) } - let(:client_two) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: wildcard_token)) } + let(:client_two) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: wildcard_token)) } let(:members_per_client) { 10 } let(:clients_entered) { Hash.new { |hash, key| hash[key] = 0 } } let(:total_members) { members_per_client * 2 } @@ -2003,7 +2003,7 @@ def connect_members_deferrables let(:client_id) { random_str.encode(Encoding::ASCII_8BIT) } context 'in connection set up' do - let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(client_id: client_id)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(default_options.merge(client_id: client_id)) } it 'is converted into UTF_8' do presence_client_one.enter @@ -2016,7 +2016,7 @@ def connect_members_deferrables end context 'in channel options' do - let(:client_one) { auto_close Ably::Realtime::Client.new(default_options) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(default_options) } it 'is converted into UTF_8' do channel_client_one.attach do @@ -2383,8 +2383,8 @@ def connect_members_deferrables let(:present_only_capability) do { channel_name => ["presence"] } end - let(:present_only_callback) { lambda { |token_params| Ably::Rest::Client.new(client_options).auth.request_token(client_id: '*', capability: present_only_capability) } } - let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: present_only_callback)) } + let(:present_only_callback) { lambda { |token_params| Ably::Internal.create_rest_client(client_options).auth.request_token(client_id: '*', capability: present_only_capability) } } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: present_only_callback)) } it 'receives presence updates for all presence events generated by the current connection and the presence map is kept up to date (#RTP17a)' do enter_client_ids = [] @@ -2609,7 +2609,7 @@ def cripple_websocket_transport let(:client_one_options) do client_options.merge(client_id: client_one_id, log_level: :error) end - let(:client_one) { auto_close Ably::Realtime::Client.new(client_one_options) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_one_options) } it 'should emit an ErrorInfo with error code 91004 (#RTP5c3)' do presence_client_one.enter @@ -2682,8 +2682,8 @@ def cripple_websocket_transport end context 'channel transitions to the FAILED state' do - let(:anonymous_client) { auto_close Ably::Realtime::Client.new(client_options.merge(log_level: :fatal)) } - let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(client_id: client_one_id, log_level: :fatal)) } + let(:anonymous_client) { auto_close Ably::Internal.create_realtime_client(client_options.merge(log_level: :fatal)) } + let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(client_id: client_one_id, log_level: :fatal)) } it 'clears the PresenceMap and local member map copy and does not emit any presence events (#RTP5a)' do presence_client_one.enter @@ -2745,10 +2745,10 @@ def cripple_websocket_transport lambda do |token_params| # Pause to allow presence updates to occur whilst disconnected sleep 1 - Ably::Rest::Client.new(client_options).auth.request_token + Ably::Internal.create_rest_client(client_options).auth.request_token end end - let(:anonymous_client) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: auth_callback)) } + let(:anonymous_client) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: auth_callback)) } it 'maintains the PresenceMap and only publishes presence event changes since the last attached state (#RTP5f)' do presence_client_one.enter do diff --git a/spec/acceptance/realtime/push_admin_spec.rb b/spec/acceptance/realtime/push_admin_spec.rb index e9dfba92b..8f2ceb84b 100644 --- a/spec/acceptance/realtime/push_admin_spec.rb +++ b/spec/acceptance/realtime/push_admin_spec.rb @@ -10,7 +10,7 @@ let(:default_options) { { key: api_key, environment: environment, protocol: protocol} } let(:client_options) { default_options } let(:client) do - Ably::Realtime::Client.new(client_options) + Ably::Internal.create_realtime_client(client_options) end let(:basic_notification_payload) do diff --git a/spec/acceptance/realtime/stats_spec.rb b/spec/acceptance/realtime/stats_spec.rb index bff42fd93..84ca16dfb 100644 --- a/spec/acceptance/realtime/stats_spec.rb +++ b/spec/acceptance/realtime/stats_spec.rb @@ -3,7 +3,7 @@ describe Ably::Realtime::Client, '#stats', :event_machine do vary_by_protocol do let(:client) do - auto_close Ably::Realtime::Client.new(key: api_key, environment: environment, protocol: protocol) + auto_close Ably::Internal.create_realtime_client(key: api_key, environment: environment, protocol: protocol) end describe 'fetching stats' do diff --git a/spec/acceptance/realtime/time_spec.rb b/spec/acceptance/realtime/time_spec.rb index c17376b3c..805befd88 100644 --- a/spec/acceptance/realtime/time_spec.rb +++ b/spec/acceptance/realtime/time_spec.rb @@ -3,7 +3,7 @@ describe Ably::Realtime::Client, '#time', :event_machine do vary_by_protocol do let(:client) do - auto_close Ably::Realtime::Client.new(key: api_key, environment: environment, protocol: protocol) + auto_close Ably::Internal.create_realtime_client(key: api_key, environment: environment, protocol: protocol) end describe 'fetching the service time' do @@ -25,7 +25,7 @@ context 'with reconfigured HTTP timeout' do let(:client) do - auto_close Ably::Realtime::Client.new(http_request_timeout: 0.0001, key: api_key, environment: environment, protocol: protocol, log_level: :fatal) + auto_close Ably::Internal.create_realtime_client(http_request_timeout: 0.0001, key: api_key, environment: environment, protocol: protocol, log_level: :fatal) end it 'should raise a timeout exception' do diff --git a/spec/acceptance/rest/auth_spec.rb b/spec/acceptance/rest/auth_spec.rb index ef07af9f2..d624fe9c2 100644 --- a/spec/acceptance/rest/auth_spec.rb +++ b/spec/acceptance/rest/auth_spec.rb @@ -29,7 +29,7 @@ def hmac_for(token_request_attributes, secret) let(:default_options) { { environment: environment, protocol: protocol } } let(:client_options) { default_options.merge(key: api_key) } let(:client) do - Ably::Rest::Client.new(client_options) + Ably::Internal.create_rest_client(client_options) end let(:auth) { client.auth } let(:content_type) do @@ -517,7 +517,7 @@ def coerce_if_time_value(field_name, value, params = {}) end context 'that returns a Token string' do - let(:second_client) { Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol) } + let(:second_client) { Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) } let(:token) { second_client.auth.request_token.token } let!(:token_details) do @@ -798,7 +798,7 @@ def coerce_if_time_value(field_name, value, params = {}) @block_called = 0 end - let(:token_client) { Ably::Rest::Client.new(default_options.merge(key: api_key, default_token_params: { ttl: 3 })) } + let(:token_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, default_token_params: { ttl: 3 })) } let(:client_options) { default_options.merge(token: token_client.auth.request_token.token, auth_callback: lambda do |token_params| @block_called += 1 @@ -819,7 +819,7 @@ def coerce_if_time_value(field_name, value, params = {}) context 'with an explicit ClientOptions client_id' do let(:client_id) { random_str } let(:client_options) { default_options.merge(auth_callback: lambda { |token_params| auth_token_object }, client_id: client_id) } - let(:auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')) } + let(:auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: 'invalid')) } context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do let(:auth_token_object) { auth_client.auth.request_token } @@ -861,7 +861,7 @@ def coerce_if_time_value(field_name, value, params = {}) it 'returns a TokenRequest that can be passed to a client that can use it for authentication without an API key' do auth_callback = proc { |token_params| subject } - client_without_api_key = Ably::Rest::Client.new(default_options.merge(auth_callback: auth_callback)) + client_without_api_key = Ably::Internal.create_rest_client(default_options.merge(auth_callback: auth_callback)) expect(client_without_api_key.auth).to be_using_token_auth expect { client_without_api_key.auth.authorize }.to_not raise_error end @@ -922,7 +922,7 @@ def coerce_if_time_value(field_name, value, params = {}) it 'uses these capabilities when Ably issues an actual token' do auth_callback = lambda { |token_params| subject } - client_without_api_key = Ably::Rest::Client.new(default_options.merge(auth_callback: auth_callback)) + client_without_api_key = Ably::Internal.create_rest_client(default_options.merge(auth_callback: auth_callback)) client_without_api_key.auth.authorize expect(client_without_api_key.auth.current_token_details.capability).to eql(capability) end @@ -939,7 +939,7 @@ def coerce_if_time_value(field_name, value, params = {}) end context 'when required fields are missing' do - let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com', protocol: protocol) } + let(:client) { Ably::Internal.create_rest_client(auth_url: 'http://example.com', protocol: protocol) } it 'should raise an exception if key secret is missing' do expect { auth.create_token_request({}, key_name: 'name') }.to raise_error Ably::Exceptions::TokenRequestFailed @@ -1033,7 +1033,7 @@ def coerce_if_time_value(field_name, value, params = {}) auth_callback = lambda do |callback| auth.create_token_request(token_attributes) end - client = Ably::Rest::Client.new(auth_callback: auth_callback, environment: environment, protocol: protocol) + client = Ably::Internal.create_rest_client(auth_callback: auth_callback, environment: environment, protocol: protocol) client.auth.authorize end end @@ -1053,7 +1053,7 @@ def coerce_if_time_value(field_name, value, params = {}) end let(:token) { token_details.token } let(:token_auth_client) do - Ably::Rest::Client.new(token: token, environment: environment, protocol: protocol) + Ably::Internal.create_rest_client(token: token, environment: environment, protocol: protocol) end it 'authenticates successfully using the provided :token' do @@ -1186,10 +1186,10 @@ def coerce_if_time_value(field_name, value, params = {}) context 'when :client_id is provided in a token' do let(:client_id) { '123' } let(:token) do - Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol).auth.request_token(client_id: client_id) + Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol).auth.request_token(client_id: client_id) end let(:client) do - Ably::Rest::Client.new(token: token, environment: environment, protocol: protocol) + Ably::Internal.create_rest_client(token: token, environment: environment, protocol: protocol) end specify '#client_id contains the client_id' do @@ -1199,7 +1199,7 @@ def coerce_if_time_value(field_name, value, params = {}) end describe '#client_id_validated?' do - let(:auth) { Ably::Rest::Client.new(default_options.merge(key: api_key)).auth } + let(:auth) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)).auth } context 'when using basic auth' do let(:client_options) { default_options.merge(key: api_key) } @@ -1293,7 +1293,7 @@ def coerce_if_time_value(field_name, value, params = {}) context 'when using JWT' do let(:auth_url) { 'https://echo.ably.io/createJWT' } let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body } - let(:client) { Ably::Rest::Client.new(token: token, environment: environment, protocol: protocol) } + let(:client) { Ably::Internal.create_rest_client(token: token, environment: environment, protocol: protocol) } it 'authenticates correctly using the JWT token generated by the echo server' do expect(client.stats).to_not be_nil() @@ -1317,7 +1317,7 @@ def coerce_if_time_value(field_name, value, params = {}) # RSA4f, RSA8c context 'when the token requested is returned with application/jwt content type' do - let(:auth_rest_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) } + let(:auth_rest_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } let(:auth_params) { { keyName: key_name, keySecret: key_secret, returnType: 'jwt' } } let(:token) { auth_rest_client.auth.request_token({ }, { auth_url: auth_url, auth_params: auth_params }).token } it 'authenticates correctly and pulls stats' do diff --git a/spec/acceptance/rest/base_spec.rb b/spec/acceptance/rest/base_spec.rb index 49fd2d5c9..b08c06070 100644 --- a/spec/acceptance/rest/base_spec.rb +++ b/spec/acceptance/rest/base_spec.rb @@ -7,7 +7,7 @@ let(:client_options) { {} } let(:client) do - Ably::Rest::Client.new(client_options.merge(key: 'appid.keyuid:keysecret', log_retries_as_info: true)) + Ably::Internal.create_rest_client(client_options.merge(key: 'appid.keyuid:keysecret', log_retries_as_info: true)) end let(:now) { Time.now - 1000 } @@ -67,13 +67,13 @@ vary_by_protocol do let(:client) do - Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) + Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) end describe 'failed requests' do context 'due to invalid Auth' do it 'should raise an UnauthorizedRequest exception with a valid error message and code' do - invalid_client = Ably::Rest::Client.new(key: 'appid.keyuid:keysecret', environment: environment) + invalid_client = Ably::Internal.create_rest_client(key: 'appid.keyuid:keysecret', environment: environment) expect { invalid_client.channel('test').publish('foo', 'choo') }.to raise_error do |error| expect(error).to be_a(Ably::Exceptions::UnauthorizedRequest) expect(error.code).to eql(40101) @@ -156,7 +156,7 @@ end context 'when NOT auth#token_renewable?' do - let(:client) { Ably::Rest::Client.new(token: 'token ID cannot be used to create a new token', environment: environment, protocol: protocol) } + let(:client) { Ably::Internal.create_rest_client(token: 'token ID cannot be used to create a new token', environment: environment, protocol: protocol) } it 'should raise an TokenExpired exception' do client.channel(channel).publish('evt', 'msg') diff --git a/spec/acceptance/rest/channel_spec.rb b/spec/acceptance/rest/channel_spec.rb index 37a4e1748..924ebae48 100644 --- a/spec/acceptance/rest/channel_spec.rb +++ b/spec/acceptance/rest/channel_spec.rb @@ -8,7 +8,7 @@ let(:default_options) { { key: api_key, environment: environment, protocol: protocol, max_frame_size: max_frame_size, max_message_size: max_message_size, idempotent_rest_publishing: false } } let(:client_options) { default_options } let(:client) do - Ably::Rest::Client.new(client_options) + Ably::Internal.create_rest_client(client_options) end let(:max_message_size) { nil } let(:max_frame_size) { nil } @@ -232,9 +232,9 @@ context 'identified clients' do context 'when authenticated with a wildcard client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: '*') } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: '*') } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Rest::Client.new(client_options) } + let(:client) { Ably::Internal.create_rest_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a valid client_id in the message' do @@ -263,9 +263,9 @@ end context 'when authenticated with a Token string with an implicit client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: 'valid').token } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid').token } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Rest::Client.new(client_options) } + let(:client) { Ably::Internal.create_rest_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'without having a confirmed identity' do @@ -296,9 +296,9 @@ end context 'when authenticated with TokenDetails with a valid client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: 'valid') } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid') } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Rest::Client.new(client_options) } + let(:client) { Ably::Internal.create_rest_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a valid client_id in the message' do @@ -333,9 +333,9 @@ end context 'when anonymous and no client_id' do - let(:token) { Ably::Rest::Client.new(default_options).auth.request_token(client_id: nil) } + let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: nil) } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Rest::Client.new(client_options) } + let(:client) { Ably::Internal.create_rest_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a client_id in the message' do diff --git a/spec/acceptance/rest/channels_spec.rb b/spec/acceptance/rest/channels_spec.rb index 6b6485e6d..82efc17d8 100644 --- a/spec/acceptance/rest/channels_spec.rb +++ b/spec/acceptance/rest/channels_spec.rb @@ -15,7 +15,7 @@ vary_by_protocol do let(:client) do - Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol) + Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) end let(:channel_name) { random_str } let(:options) { { key: 'value' } } diff --git a/spec/acceptance/rest/client_spec.rb b/spec/acceptance/rest/client_spec.rb index 219ba75bb..2d0c89b6c 100644 --- a/spec/acceptance/rest/client_spec.rb +++ b/spec/acceptance/rest/client_spec.rb @@ -7,7 +7,7 @@ let(:default_options) { { environment: environment, protocol: protocol, log_retries_as_info: true } } let(:client_options) { default_options } - let(:client) { Ably::Rest::Client.new(client_options) } + let(:client) { Ably::Internal.create_rest_client(client_options) } http_defaults = Ably::Rest::Client::HTTP_DEFAULTS @@ -20,7 +20,7 @@ def encode64(text) let(:token_request) { client.auth.create_token_request({}, key_name: key_name, key_secret: key_secret, client_id: client_id) } context 'with only an API key' do - let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key)) } + let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: api_key)) } it 'uses basic authentication' do expect(client.auth).to be_using_basic_auth @@ -28,7 +28,7 @@ def encode64(text) end context 'with an invalid API key' do - let(:client) { Ably::Rest::Client.new(client_options.merge(key: 'app.key:secret', log_level: :fatal)) } + let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: 'app.key:secret', log_level: :fatal)) } it 'logs an entry with a help href url matching the code #TI5' do begin @@ -41,7 +41,7 @@ def encode64(text) end context 'with an explicit string :token' do - let(:client) { Ably::Rest::Client.new(client_options.merge(token: random_str)) } + let(:client) { Ably::Internal.create_rest_client(client_options.merge(token: random_str)) } it 'uses token authentication' do expect(client.auth).to be_using_token_auth @@ -49,7 +49,7 @@ def encode64(text) end context 'with :use_token_auth set to true' do - let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, use_token_auth: true)) } + let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: api_key, use_token_auth: true)) } it 'uses token authentication' do expect(client.auth).to be_using_token_auth @@ -57,7 +57,7 @@ def encode64(text) end context 'with a non string :client_id' do - let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: 1)) } + let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: api_key, client_id: 1)) } it 'raises an ArgumentError' do expect { client.auth }.to raise_error ArgumentError, /client_id.*String/ @@ -66,12 +66,12 @@ def encode64(text) context 'with an invalid wildcard "*" :client_id' do it 'raises an exception' do - expect { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError + expect { Ably::Internal.create_rest_client(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError end end context 'with an :auth_callback lambda' do - let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: lambda { |token_params| token_request })) } + let(:client) { Ably::Internal.create_rest_client(client_options.merge(auth_callback: lambda { |token_params| token_request })) } it 'calls the auth lambda to get a new token' do expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details } @@ -85,7 +85,7 @@ def encode64(text) context 'with :default_token_params' do let(:client) do - Ably::Rest::Client.new(client_options.merge( + Ably::Internal.create_rest_client(client_options.merge( default_token_params: { client_id: 'bob' }, use_token_auth: true, key: api_key @@ -99,7 +99,7 @@ def encode64(text) end context 'with an :auth_callback lambda (clientId provided in library options instead of as a token_request param)' do - let(:client) { Ably::Rest::Client.new(client_options.merge(client_id: client_id, auth_callback: lambda { |token_params| token_request })) } + let(:client) { Ably::Internal.create_rest_client(client_options.merge(client_id: client_id, auth_callback: lambda { |token_params| token_request })) } let(:token_request) { client.auth.create_token_request({}, key_name: key_name, key_secret: key_secret) } it 'correctly sets the clientId on the token' do @@ -184,7 +184,7 @@ def encode64(text) context 'using tokens' do let(:client) do - Ably::Rest::Client.new(client_options.merge(auth_callback: lambda do |token_params| + Ably::Internal.create_rest_client(client_options.merge(auth_callback: lambda do |token_params| @request_index ||= 0 @request_index += 1 send("token_request_#{@request_index > 2 ? 'next' : @request_index}") @@ -1349,8 +1349,8 @@ def encode64(text) let(:token_params) { { nonce: "samenonce_#{protocol}", timestamp: Time.now.to_i } } it 'includes request_id in UnauthorizedRequest error due to replayed nonce' do - client1 = Ably::Rest::Client.new(default_options.merge(key: api_key)) - client2 = Ably::Rest::Client.new(default_options.merge(key: api_key, add_request_ids: true)) + client1 = Ably::Internal.create_rest_client(default_options.merge(key: api_key)) + client2 = Ably::Internal.create_rest_client(default_options.merge(key: api_key, add_request_ids: true)) expect { client1.auth.request_token(token_params) }.not_to raise_error begin client2.auth.request_token(token_params) diff --git a/spec/acceptance/rest/encoders_spec.rb b/spec/acceptance/rest/encoders_spec.rb index f09ac7209..36a35a771 100644 --- a/spec/acceptance/rest/encoders_spec.rb +++ b/spec/acceptance/rest/encoders_spec.rb @@ -4,7 +4,7 @@ describe Ably::Models::MessageEncoders do let(:default_client_options) { { key: api_key, environment: environment } } - let(:client) { Ably::Rest::Client.new(default_client_options.merge(protocol: protocol)) } + let(:client) { Ably::Internal.create_rest_client(default_client_options.merge(protocol: protocol)) } let(:channel_options) { {} } let(:channel) { client.channel('test', channel_options) } let(:response) { instance_double('Faraday::Response', status: 201) } diff --git a/spec/acceptance/rest/message_spec.rb b/spec/acceptance/rest/message_spec.rb index b9ac0ac67..550066896 100644 --- a/spec/acceptance/rest/message_spec.rb +++ b/spec/acceptance/rest/message_spec.rb @@ -9,8 +9,8 @@ vary_by_protocol do let(:default_client_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_client_options } - let(:client) { Ably::Rest::Client.new(client_options) } - let(:other_client) { Ably::Rest::Client.new(client_options) } + let(:client) { Ably::Internal.create_rest_client(client_options) } + let(:other_client) { Ably::Internal.create_rest_client(client_options) } let(:channel) { client.channel(random_str) } context 'publishing with an ASCII_8BIT message name' do @@ -206,16 +206,16 @@ specify 'idempotent publishing is set as per clientOptions' do # set idempotent_rest_publishing to false - client = Ably::Rest::Client.new(key: api_key, protocol: protocol, idempotent_rest_publishing: false) + client = Ably::Internal.create_rest_client(key: api_key, protocol: protocol, idempotent_rest_publishing: false) expect(client.idempotent_rest_publishing).to be_falsey # set idempotent_rest_publishing to true - client = Ably::Rest::Client.new(key: api_key, protocol: protocol, idempotent_rest_publishing: true) + client = Ably::Internal.create_rest_client(key: api_key, protocol: protocol, idempotent_rest_publishing: true) expect(client.idempotent_rest_publishing).to be_truthy end specify 'idempotent publishing is enabled by default (#TO3n)' do - client = Ably::Rest::Client.new(key: api_key, protocol: protocol) + client = Ably::Internal.create_rest_client(key: api_key, protocol: protocol) expect(client.idempotent_rest_publishing).to be_truthy end @@ -436,7 +436,7 @@ def self.add_tests_for_data(data) context 'when retrieving #history with a different protocol' do let(:other_protocol) { protocol == :msgpack ? :json : :msgpack } - let(:other_client) { Ably::Rest::Client.new(default_client_options.merge(protocol: other_protocol)) } + let(:other_client) { Ably::Internal.create_rest_client(default_client_options.merge(protocol: other_protocol)) } let(:other_client_channel) { other_client.channel(channel_name, cipher: cipher_options) } before do diff --git a/spec/acceptance/rest/presence_spec.rb b/spec/acceptance/rest/presence_spec.rb index b0c61c8e0..889e3ec01 100644 --- a/spec/acceptance/rest/presence_spec.rb +++ b/spec/acceptance/rest/presence_spec.rb @@ -8,7 +8,7 @@ let(:default_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_options } let(:client) do - Ably::Rest::Client.new(client_options) + Ably::Internal.create_rest_client(client_options) end let(:fixtures) do @@ -201,7 +201,7 @@ client.endpoint end let(:client) do - Ably::Rest::Client.new(key: "#{user}:#{secret}") + Ably::Internal.create_rest_client(key: "#{user}:#{secret}") end let(:history_options) do { @@ -326,7 +326,7 @@ def message(client_id, messages) client.endpoint end let(:client) do - Ably::Rest::Client.new(client_options.merge(key: "#{user}:#{secret}")) + Ably::Internal.create_rest_client(client_options.merge(key: "#{user}:#{secret}")) end let(:data) { random_str(32) } diff --git a/spec/acceptance/rest/push_admin_spec.rb b/spec/acceptance/rest/push_admin_spec.rb index a5e1433f0..4b042dcef 100644 --- a/spec/acceptance/rest/push_admin_spec.rb +++ b/spec/acceptance/rest/push_admin_spec.rb @@ -8,7 +8,7 @@ let(:default_options) { { key: api_key, environment: environment, protocol: protocol} } let(:client_options) { default_options } let(:client) do - Ably::Rest::Client.new(client_options) + Ably::Internal.create_rest_client(client_options) end let(:basic_notification_payload) do diff --git a/spec/acceptance/rest/stats_spec.rb b/spec/acceptance/rest/stats_spec.rb index 1a4d81a03..a1cb46136 100644 --- a/spec/acceptance/rest/stats_spec.rb +++ b/spec/acceptance/rest/stats_spec.rb @@ -48,7 +48,7 @@ end vary_by_protocol do - let(:client) { Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol) } + let(:client) { Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) } describe 'fetching application stats' do it 'returns a PaginatedResult object' do diff --git a/spec/acceptance/rest/time_spec.rb b/spec/acceptance/rest/time_spec.rb index 7972c80b1..fd1c9a55d 100644 --- a/spec/acceptance/rest/time_spec.rb +++ b/spec/acceptance/rest/time_spec.rb @@ -3,7 +3,7 @@ describe Ably::Rest::Client, '#time' do vary_by_protocol do let(:client) do - Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol) + Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) end describe 'fetching the service time' do @@ -13,7 +13,7 @@ context 'with reconfigured HTTP timeout' do let(:client) do - Ably::Rest::Client.new(http_request_timeout: 0.0001, key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) + Ably::Internal.create_rest_client(http_request_timeout: 0.0001, key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) end it 'should raise a timeout exception' do diff --git a/spec/support/test_app.rb b/spec/support/test_app.rb index 89b1b5cfc..f0e4cfc93 100644 --- a/spec/support/test_app.rb +++ b/spec/support/test_app.rb @@ -94,13 +94,13 @@ def realtime_host end def create_test_stats(stats) - client = Ably::Rest::Client.new(key: api_key, environment: environment) + client = Ably::Internal.create_rest_client(key: api_key, environment: environment) response = client.post('/stats', stats) raise "Could not create stats fixtures. Ably responded with status #{response.status}\n#{response.body}" unless (200..299).include?(response.status) end private def sandbox_client - @sandbox_client ||= Ably::Rest::Client.new(key: 'app.key:secret', tls: true, environment: environment) + @sandbox_client ||= Ably::Internal.create_rest_client(key: 'app.key:secret', tls: true, environment: environment) end end diff --git a/spec/unit/pubsub/direct_construction_spec.rb b/spec/unit/pubsub/direct_construction_spec.rb new file mode 100644 index 000000000..945893fbd --- /dev/null +++ b/spec/unit/pubsub/direct_construction_spec.rb @@ -0,0 +1,80 @@ +# encoding: utf-8 +require 'spec_helper' +require 'ably/pubsub/server' + +# A client's side is declared by the package it was created from, so a client constructed +# directly declares none and the platform cannot classify it for MAU billing. The classes +# therefore refuse direct construction, and the per-side packages construct through +# Ably::Internal instead. These specs guard both halves: the refusals must stay in place, +# and the internal entry points must keep producing usable clients. The Ably::Rest and +# Ably::Realtime module aliases refuse too, covered in their own module specs. +describe 'direct construction' do + let(:api_key) { 'appid.keyuid:keysecret' } + + refused = { + 'Ably::Rest::Client.new' => -> (key) { Ably::Rest::Client.new(key) }, + 'Ably::Realtime::Client.new' => -> (key) { Ably::Realtime::Client.new(key) }, + } + + refused.each do |subject_name, construct| + describe subject_name do + it 'raises DirectConstructionNotSupported' do + expect { construct.call(api_key) } + .to raise_error(Ably::Exceptions::DirectConstructionNotSupported) + end + + it 'names itself and points at a factory function' do + expect { construct.call(api_key) }.to raise_error do |error| + expect(error.message).to include(subject_name) + expect(error.message).to match(/Ably::PubSub::Server\.create_(http|realtime)_client/) + end + end + + # Hash and String options take different paths through the constructors, and a + # refusal that only covered one of them would leave the other reachable. + it 'raises for Hash options too' do + expect { construct.call(key: api_key) } + .to raise_error(Ably::Exceptions::DirectConstructionNotSupported) + end + end + end + + describe 'Ably::Internal' do + it 'creates a usable REST client' do + client = Ably::Internal.create_rest_client(key: api_key) + expect(client).to be_a(Ably::Rest::Client) + expect(client.auth.key).to eql(api_key) + end + + it 'creates a usable realtime client' do + client = Ably::Internal.create_realtime_client(key: api_key, auto_connect: false) + expect(client).to be_a(Ably::Realtime::Client) + expect(client.rest_client).to be_a(Ably::Rest::Client) + end + + # The realtime client builds its own REST client; if that call went back through the + # public constructor, every realtime client would fail to build. + it 'is what the realtime client uses to build its REST client' do + expect(Ably::Internal).to receive(:create_rest_client).once.and_call_original + Ably::Internal.create_realtime_client(key: api_key, auto_connect: false) + end + + it 'declares no side of its own' do + client = Ably::Internal.create_rest_client(key: api_key) + expect(client.agent).to_not include(Ably::PubSub::Server::SERVER_AGENT_IDENTIFIER) + end + end + + # The whole point of the refusal: the supported path still works, and carries the side. + describe 'the supported path' do + it 'creates an HTTP client declaring the server side' do + client = Ably::PubSub::Server.create_http_client(api_key) + expect(client.agent).to include(Ably::PubSub::Server::SERVER_AGENT_IDENTIFIER) + end + + it 'creates a realtime client declaring the server side' do + client = Ably::PubSub::Server.create_realtime_client(key: api_key, auto_connect: false) + expect(client.rest_client.agent).to include(Ably::PubSub::Server::SERVER_AGENT_IDENTIFIER) + end + end +end diff --git a/spec/unit/pubsub/packaging_spec.rb b/spec/unit/pubsub/packaging_spec.rb index 7eb1675bc..8a7d8e972 100644 --- a/spec/unit/pubsub/packaging_spec.rb +++ b/spec/unit/pubsub/packaging_spec.rb @@ -39,6 +39,27 @@ expect(core_spec.files.grep(%r{\Alib/submodules/})).to be_empty end + # spec.files comes from `git ls-files`, so a Ruby file that has not been added to the + # index is silently left out of the built gem, and the omission surfaces only as a + # LoadError or NameError on a consumer's machine after publish. + source_files_on_disk = lambda do |dir| + Dir.glob(File.join(repo_root, dir, 'lib/**/*.rb')) + .map { |path| path.sub(File.join(repo_root, dir, ''), '') } + .reject { |path| path.start_with?('lib/submodules/') } + end + + it 'core ships every Ruby file under core/lib' do + on_disk = source_files_on_disk.call('core') + expect(on_disk).to_not be_empty + expect(on_disk - core_spec.files).to be_empty + end + + it 'server ships every Ruby file under server/lib' do + on_disk = source_files_on_disk.call('server') + expect(on_disk).to_not be_empty + expect(on_disk - server_spec.files).to be_empty + end + it 'releases both gems at one version (lockstep)' do expect(server_spec.version).to eql(core_spec.version) end diff --git a/spec/unit/realtime/channel_spec.rb b/spec/unit/realtime/channel_spec.rb index 97857b4a0..8fb596cde 100644 --- a/spec/unit/realtime/channel_spec.rb +++ b/spec/unit/realtime/channel_spec.rb @@ -3,7 +3,7 @@ require 'shared/protocol_msgbus_behaviour' describe Ably::Realtime::Channel do - let(:client) { Ably::Realtime::Client.new(token: 'valid') } + let(:client) { Ably::Internal.create_realtime_client(token: 'valid') } let(:channel_name) { 'test' } subject do diff --git a/spec/unit/realtime/client_spec.rb b/spec/unit/realtime/client_spec.rb index 48eb81c71..244ff8256 100644 --- a/spec/unit/realtime/client_spec.rb +++ b/spec/unit/realtime/client_spec.rb @@ -4,7 +4,7 @@ describe Ably::Realtime::Client do subject(:realtime_client) do - Ably::Realtime::Client.new(client_options) + Ably::Internal.create_realtime_client(client_options) end it_behaves_like 'a client initializer' @@ -14,7 +14,7 @@ it 'passes on the options to the initializer' do rest_client = instance_double('Ably::Rest::Client', auth: instance_double('Ably::Auth'), options: client_options, environment: 'production', use_tls?: true, custom_tls_port: nil) - expect(Ably::Rest::Client).to receive(:new).with(hash_including(client_options)).and_return(rest_client) + expect(Ably::Internal).to receive(:create_rest_client).with(hash_including(client_options)).and_return(rest_client) realtime_client end diff --git a/spec/unit/realtime/realtime_spec.rb b/spec/unit/realtime/realtime_spec.rb index d0d308089..2703c6ce6 100644 --- a/spec/unit/realtime/realtime_spec.rb +++ b/spec/unit/realtime/realtime_spec.rb @@ -3,11 +3,10 @@ describe Ably::Realtime do let(:options) { { key: 'app.key:secret', auto_connect: false } } - specify 'constructor returns an Ably::Realtime::Client' do - expect(Ably::Realtime.new(options)).to be_instance_of(Ably::Realtime::Client) - end - - after(:all) do - sleep 1 # let realtime library shut down any open clients + # This was a convenience alias for the Ably::Realtime::Client constructor. It now + # refuses, because a client's side is declared by the package it was created from. + specify 'constructor refuses and points at the factory function' do + expect { Ably::Realtime.new(options) } + .to raise_error(Ably::Exceptions::DirectConstructionNotSupported, /create_realtime_client/) end end diff --git a/spec/unit/rest/client_spec.rb b/spec/unit/rest/client_spec.rb index 7b7da1638..4ceb082d8 100644 --- a/spec/unit/rest/client_spec.rb +++ b/spec/unit/rest/client_spec.rb @@ -4,7 +4,7 @@ describe Ably::Rest::Client do subject do - Ably::Rest::Client.new(client_options) + Ably::Internal.create_rest_client(client_options) end it_behaves_like 'a client initializer' diff --git a/spec/unit/rest/rest_spec.rb b/spec/unit/rest/rest_spec.rb index 81e172372..e53d59b35 100644 --- a/spec/unit/rest/rest_spec.rb +++ b/spec/unit/rest/rest_spec.rb @@ -4,7 +4,10 @@ describe Ably::Rest do let(:options) { { key: 'app.key:secret' } } - specify 'constructor returns an Ably::Rest::Client' do - expect(Ably::Rest.new(options)).to be_instance_of(Ably::Rest::Client) + # This was a convenience alias for the Ably::Rest::Client constructor. It now refuses, + # because a client's side is declared by the package it was created from. + specify 'constructor refuses and points at the factory function' do + expect { Ably::Rest.new(options) } + .to raise_error(Ably::Exceptions::DirectConstructionNotSupported, /create_http_client/) end end From 60b4e361183dc1a5cb27c6cd333fb7346c543758 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 14 Sep 2026 21:47:13 +0100 Subject: [PATCH 12/13] Fix REST presence query params to use correct camelCase format (clientId, connectionId). --- core/lib/ably/rest/presence.rb | 4 +++- spec/acceptance/rest/presence_spec.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/core/lib/ably/rest/presence.rb b/core/lib/ably/rest/presence.rb index 3fa3159ca..afb701775 100644 --- a/core/lib/ably/rest/presence.rb +++ b/core/lib/ably/rest/presence.rb @@ -51,7 +51,9 @@ def get(options = {}) async_blocking_operations: options.delete(:async_blocking_operations), } - response = client.get(base_path, options) + # The REST API names these query params clientId and connectionId (RSP3a2, RSP3a3), + # so the idiomatic snake_case options have to be converted before they are sent. + response = client.get(base_path, IdiomaticRubyWrapper(options).as_json) Ably::Models::PaginatedResult.new(response, base_path, client, paginated_options) do |presence_message| presence_message.tap do |message| diff --git a/spec/acceptance/rest/presence_spec.rb b/spec/acceptance/rest/presence_spec.rb index 889e3ec01..7386b39b3 100644 --- a/spec/acceptance/rest/presence_spec.rb +++ b/spec/acceptance/rest/presence_spec.rb @@ -88,6 +88,33 @@ end end + # The REST API names these params clientId and connectionId. Sending the snake_case + # option names instead is not an error the server reports: it ignores unknown query + # params and returns the unfiltered presence set, so only the wire format catches it. + context 'filter query params', webmock: true do + let(:channel_name) { random_str } + let(:channel) { client.channels.get(channel_name) } + let(:endpoint) { client.endpoint } + + def stub_presence_get(query) + stub_request(:get, "#{endpoint}/channels/#{URI.encode_www_form_component(channel_name)}/presence"). + with(query: query). + to_return(body: '{}', headers: { 'Content-Type' => 'application/json' }) + end + + it 'sends :client_id as clientId' do + stub = stub_presence_get('limit' => '100', 'clientId' => 'bob') + channel.presence.get(client_id: 'bob') + expect(stub).to have_been_requested + end + + it 'sends :connection_id as connectionId' do + stub = stub_presence_get('limit' => '100', 'connectionId' => 'conn-1') + channel.presence.get(connection_id: 'conn-1') + expect(stub).to have_been_requested + end + end + context 'with :client_id option' do let(:client_id) { non_encoded_fixtures.first[:client_id] } let(:presence_page) { fixtures_channel.presence.get(client_id: client_id) } From 6e6123f69314a8738dcc29f1430eb4a069c065d8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 16 Sep 2026 12:43:00 +0100 Subject: [PATCH 13/13] Move client namespaces under Ably::PubSub and rename Rest to Http Ably::Rest::* becomes Ably::PubSub::Http::* and Ably::Realtime::* becomes Ably::PubSub::Realtime::*, so the namespace matches the Ably::PubSub::Server.create_http_client factory that constructs these clients. Files move to lib/ably/pubsub/{http,realtime} to match. - Add lib/ably/pubsub.rb to open the namespace. It is required before the client files because 22 of them declare into it with the compact `module Ably::PubSub::Http` form, which needs Ably::PubSub to exist. - Rename Ably::Internal.create_rest_client to create_http_client, and the spec/{unit,acceptance}/rest directories to http. - Narrow the packaging assertion that core ships nothing under lib/ably/pubsub to the server subtree alone. Both gems now write into that prefix, which makes the no-overlap rule load-bearing rather than incidental. - Fix five YARD links to Ably::Models::Rest::Channel, a namespace that never existed. - Rewrite the 2.0 section of UPDATING.md around before/after examples and drop its draft status. Ably::Auth, Ably::Models::*, Ably::Exceptions::*, Ably::Logger and every client option, including :rest_host, are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- UPDATING.md | 107 ++- core/README.md | 2 +- core/lib/ably.rb | 5 +- core/lib/ably/auth.rb | 6 +- core/lib/ably/internal.rb | 28 +- core/lib/ably/logger.rb | 6 +- core/lib/ably/models/channel_details.rb | 2 +- core/lib/ably/models/channel_metrics.rb | 2 +- core/lib/ably/models/channel_occupancy.rb | 2 +- core/lib/ably/models/channel_state_change.rb | 14 +- core/lib/ably/models/channel_status.rb | 2 +- core/lib/ably/models/cipher_params.rb | 2 +- core/lib/ably/models/connection_details.rb | 4 +- .../ably/models/connection_state_change.rb | 12 +- .../ably/models/http_paginated_response.rb | 6 +- core/lib/ably/models/message.rb | 2 +- core/lib/ably/models/paginated_result.rb | 4 +- core/lib/ably/models/presence_message.rb | 2 +- core/lib/ably/models/protocol_message.rb | 2 +- core/lib/ably/modules/ably.rb | 2 +- core/lib/ably/modules/channels_collection.rb | 2 +- core/lib/ably/modules/http_helpers.rb | 22 +- core/lib/ably/pubsub.rb | 11 + core/lib/ably/pubsub/http.rb | 38 + core/lib/ably/pubsub/http/channel.rb | 196 +++++ .../http}/channel/push_channel.rb | 6 +- core/lib/ably/pubsub/http/channels.rb | 49 ++ core/lib/ably/pubsub/http/client.rb | 743 ++++++++++++++++++ .../ably/pubsub/http/middleware/encoder.rb | 51 ++ .../ably/pubsub/http/middleware/exceptions.rb | 55 ++ .../http/middleware/external_exceptions.rb | 26 + .../fail_if_unsupported_mime_type.rb | 24 + .../lib/ably/pubsub/http/middleware/logger.rb | 60 ++ .../ably/pubsub/http/middleware/parse_json.rb | 29 + .../http/middleware/parse_message_pack.rb | 45 ++ core/lib/ably/pubsub/http/presence.rb | 117 +++ core/lib/ably/pubsub/http/push.rb | 27 + .../ably/{rest => pubsub/http}/push/admin.rb | 10 +- .../http}/push/channel_subscriptions.rb | 2 +- .../http}/push/device_registrations.rb | 2 +- core/lib/ably/pubsub/realtime.rb | 58 ++ core/lib/ably/pubsub/realtime/auth.rb | 285 +++++++ core/lib/ably/pubsub/realtime/channel.rb | 442 +++++++++++ .../realtime/channel/channel_manager.rb | 4 +- .../realtime/channel/channel_properties.rb | 10 +- .../realtime/channel/channel_state_machine.rb | 4 +- .../realtime/channel/publisher.rb | 2 +- .../realtime/channel/push_channel.rb | 4 +- core/lib/ably/pubsub/realtime/channels.rb | 73 ++ core/lib/ably/pubsub/realtime/client.rb | 379 +++++++++ .../client/incoming_message_dispatcher.rb | 8 +- .../client/outgoing_message_dispatcher.rb | 4 +- core/lib/ably/pubsub/realtime/connection.rb | 740 +++++++++++++++++ .../realtime/connection/connection_manager.rb | 12 +- .../connection/connection_state_machine.rb | 4 +- .../connection/websocket_transport.rb | 2 +- .../realtime/models/nil_channel.rb | 6 +- .../ably/{ => pubsub}/realtime/presence.rb | 46 +- .../realtime/presence/members_map.rb | 4 +- .../realtime/presence/presence_manager.rb | 6 +- .../presence/presence_state_machine.rb | 4 +- core/lib/ably/pubsub/realtime/push.rb | 27 + .../ably/{ => pubsub}/realtime/push/admin.rb | 16 +- .../realtime/push/channel_subscriptions.rb | 12 +- .../realtime/push/device_registrations.rb | 12 +- .../pubsub/realtime/recovery_key_context.rb | 38 + core/lib/ably/realtime.rb | 56 -- core/lib/ably/realtime/auth.rb | 283 ------- core/lib/ably/realtime/channel.rb | 440 ----------- core/lib/ably/realtime/channels.rb | 71 -- core/lib/ably/realtime/client.rb | 377 --------- core/lib/ably/realtime/connection.rb | 738 ----------------- core/lib/ably/realtime/push.rb | 25 - .../lib/ably/realtime/recovery_key_context.rb | 36 - core/lib/ably/rest.rb | 36 - core/lib/ably/rest/channel.rb | 194 ----- core/lib/ably/rest/channels.rb | 47 -- core/lib/ably/rest/client.rb | 741 ----------------- core/lib/ably/rest/middleware/encoder.rb | 49 -- core/lib/ably/rest/middleware/exceptions.rb | 53 -- .../rest/middleware/external_exceptions.rb | 24 - .../fail_if_unsupported_mime_type.rb | 22 - core/lib/ably/rest/middleware/logger.rb | 58 -- core/lib/ably/rest/middleware/parse_json.rb | 27 - .../rest/middleware/parse_message_pack.rb | 43 - core/lib/ably/rest/presence.rb | 115 --- core/lib/ably/rest/push.rb | 25 - server/README.md | 2 +- server/lib/ably/pubsub/server.rb | 10 +- spec/acceptance/{rest => http}/auth_spec.rb | 28 +- spec/acceptance/{rest => http}/base_spec.rb | 10 +- .../acceptance/{rest => http}/channel_spec.rb | 32 +- .../{rest => http}/channels_spec.rb | 10 +- spec/acceptance/{rest => http}/client_spec.rb | 60 +- .../{rest => http}/encoders_spec.rb | 2 +- .../acceptance/{rest => http}/message_spec.rb | 14 +- .../{rest => http}/presence_spec.rb | 8 +- .../{rest => http}/push_admin_spec.rb | 4 +- spec/acceptance/{rest => http}/stats_spec.rb | 4 +- spec/acceptance/{rest => http}/time_spec.rb | 6 +- spec/acceptance/realtime/auth_spec.rb | 16 +- .../realtime/channel_history_spec.rb | 2 +- spec/acceptance/realtime/channel_spec.rb | 30 +- spec/acceptance/realtime/channels_spec.rb | 8 +- spec/acceptance/realtime/client_spec.rb | 12 +- .../realtime/connection_failures_spec.rb | 22 +- spec/acceptance/realtime/connection_spec.rb | 22 +- spec/acceptance/realtime/message_spec.rb | 12 +- .../realtime/presence_history_spec.rb | 2 +- spec/acceptance/realtime/presence_spec.rb | 14 +- spec/acceptance/realtime/push_admin_spec.rb | 4 +- spec/acceptance/realtime/stats_spec.rb | 2 +- spec/acceptance/realtime/time_spec.rb | 2 +- spec/shared/client_initializer_behaviour.rb | 2 +- spec/spec_helper.rb | 2 +- ..._retry.rb => http_testapp_before_retry.rb} | 0 spec/support/markdown_spec_formatter.rb | 2 +- spec/support/test_app.rb | 4 +- spec/unit/{rest => http}/channel_spec.rb | 6 +- spec/unit/{rest => http}/channels_spec.rb | 16 +- spec/unit/{rest => http}/client_spec.rb | 12 +- .../{rest/rest_spec.rb => http/http_spec.rb} | 6 +- spec/unit/{rest => http}/push_channel_spec.rb | 12 +- spec/unit/logger_spec.rb | 12 +- .../unit/models/http_paginated_result_spec.rb | 6 +- .../models/message_encoders/base64_spec.rb | 2 +- .../models/message_encoders/cipher_spec.rb | 2 +- .../unit/models/message_encoders/json_spec.rb | 2 +- .../unit/models/message_encoders/utf8_spec.rb | 2 +- spec/unit/models/paginated_result_spec.rb | 6 +- spec/unit/pubsub/direct_construction_spec.rb | 20 +- spec/unit/pubsub/packaging_spec.rb | 16 +- spec/unit/pubsub/server_spec.rb | 6 +- spec/unit/realtime/channel_spec.rb | 4 +- spec/unit/realtime/channels_spec.rb | 18 +- spec/unit/realtime/client_spec.rb | 8 +- spec/unit/realtime/connection_spec.rb | 6 +- .../incoming_message_dispatcher_spec.rb | 8 +- spec/unit/realtime/presence_spec.rb | 6 +- spec/unit/realtime/push_channel_spec.rb | 12 +- spec/unit/realtime/realtime_spec.rb | 6 +- .../realtime/recovery_key_context_spec.rb | 10 +- spec/unit/realtime/safe_deferrable_spec.rb | 2 +- .../unit/realtime/websocket_transport_spec.rb | 12 +- 144 files changed, 4046 insertions(+), 3909 deletions(-) create mode 100644 core/lib/ably/pubsub.rb create mode 100644 core/lib/ably/pubsub/http.rb create mode 100644 core/lib/ably/pubsub/http/channel.rb rename core/lib/ably/{rest => pubsub/http}/channel/push_channel.rb (92%) create mode 100644 core/lib/ably/pubsub/http/channels.rb create mode 100644 core/lib/ably/pubsub/http/client.rb create mode 100644 core/lib/ably/pubsub/http/middleware/encoder.rb create mode 100644 core/lib/ably/pubsub/http/middleware/exceptions.rb create mode 100644 core/lib/ably/pubsub/http/middleware/external_exceptions.rb create mode 100644 core/lib/ably/pubsub/http/middleware/fail_if_unsupported_mime_type.rb create mode 100644 core/lib/ably/pubsub/http/middleware/logger.rb create mode 100644 core/lib/ably/pubsub/http/middleware/parse_json.rb create mode 100644 core/lib/ably/pubsub/http/middleware/parse_message_pack.rb create mode 100644 core/lib/ably/pubsub/http/presence.rb create mode 100644 core/lib/ably/pubsub/http/push.rb rename core/lib/ably/{rest => pubsub/http}/push/admin.rb (88%) rename core/lib/ably/{rest => pubsub/http}/push/channel_subscriptions.rb (99%) rename core/lib/ably/{rest => pubsub/http}/push/device_registrations.rb (99%) create mode 100644 core/lib/ably/pubsub/realtime.rb create mode 100644 core/lib/ably/pubsub/realtime/auth.rb create mode 100644 core/lib/ably/pubsub/realtime/channel.rb rename core/lib/ably/{ => pubsub}/realtime/channel/channel_manager.rb (99%) rename core/lib/ably/{ => pubsub}/realtime/channel/channel_properties.rb (71%) rename core/lib/ably/{ => pubsub}/realtime/channel/channel_state_machine.rb (97%) rename core/lib/ably/{ => pubsub}/realtime/channel/publisher.rb (99%) rename core/lib/ably/{ => pubsub}/realtime/channel/push_channel.rb (95%) create mode 100644 core/lib/ably/pubsub/realtime/channels.rb create mode 100644 core/lib/ably/pubsub/realtime/client.rb rename core/lib/ably/{ => pubsub}/realtime/client/incoming_message_dispatcher.rb (96%) rename core/lib/ably/{ => pubsub}/realtime/client/outgoing_message_dispatcher.rb (97%) create mode 100644 core/lib/ably/pubsub/realtime/connection.rb rename core/lib/ably/{ => pubsub}/realtime/connection/connection_manager.rb (97%) rename core/lib/ably/{ => pubsub}/realtime/connection/connection_state_machine.rb (98%) rename core/lib/ably/{ => pubsub}/realtime/connection/websocket_transport.rb (99%) rename core/lib/ably/{ => pubsub}/realtime/models/nil_channel.rb (76%) rename core/lib/ably/{ => pubsub}/realtime/presence.rb (91%) rename core/lib/ably/{ => pubsub}/realtime/presence/members_map.rb (99%) rename core/lib/ably/{ => pubsub}/realtime/presence/presence_manager.rb (93%) rename core/lib/ably/{ => pubsub}/realtime/presence/presence_state_machine.rb (94%) create mode 100644 core/lib/ably/pubsub/realtime/push.rb rename core/lib/ably/{ => pubsub}/realtime/push/admin.rb (79%) rename core/lib/ably/{ => pubsub}/realtime/push/channel_subscriptions.rb (90%) rename core/lib/ably/{ => pubsub}/realtime/push/device_registrations.rb (89%) create mode 100644 core/lib/ably/pubsub/realtime/recovery_key_context.rb delete mode 100644 core/lib/ably/realtime.rb delete mode 100644 core/lib/ably/realtime/auth.rb delete mode 100644 core/lib/ably/realtime/channel.rb delete mode 100644 core/lib/ably/realtime/channels.rb delete mode 100644 core/lib/ably/realtime/client.rb delete mode 100644 core/lib/ably/realtime/connection.rb delete mode 100644 core/lib/ably/realtime/push.rb delete mode 100644 core/lib/ably/realtime/recovery_key_context.rb delete mode 100644 core/lib/ably/rest.rb delete mode 100644 core/lib/ably/rest/channel.rb delete mode 100644 core/lib/ably/rest/channels.rb delete mode 100644 core/lib/ably/rest/client.rb delete mode 100644 core/lib/ably/rest/middleware/encoder.rb delete mode 100644 core/lib/ably/rest/middleware/exceptions.rb delete mode 100644 core/lib/ably/rest/middleware/external_exceptions.rb delete mode 100644 core/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb delete mode 100644 core/lib/ably/rest/middleware/logger.rb delete mode 100644 core/lib/ably/rest/middleware/parse_json.rb delete mode 100644 core/lib/ably/rest/middleware/parse_message_pack.rb delete mode 100644 core/lib/ably/rest/presence.rb delete mode 100644 core/lib/ably/rest/push.rb rename spec/acceptance/{rest => http}/auth_spec.rb (98%) rename spec/acceptance/{rest => http}/base_spec.rb (95%) rename spec/acceptance/{rest => http}/channel_spec.rb (95%) rename spec/acceptance/{rest => http}/channels_spec.rb (91%) rename spec/acceptance/{rest => http}/client_spec.rb (96%) rename spec/acceptance/{rest => http}/encoders_spec.rb (98%) rename spec/acceptance/{rest => http}/message_spec.rb (97%) rename spec/acceptance/{rest => http}/presence_spec.rb (98%) rename spec/acceptance/{rest => http}/push_admin_spec.rb (99%) rename spec/acceptance/{rest => http}/stats_spec.rb (98%) rename spec/acceptance/{rest => http}/time_spec.rb (78%) rename spec/support/{rest_testapp_before_retry.rb => http_testapp_before_retry.rb} (100%) rename spec/unit/{rest => http}/channel_spec.rb (97%) rename spec/unit/{rest => http}/channels_spec.rb (86%) rename spec/unit/{rest => http}/client_spec.rb (90%) rename spec/unit/{rest/rest_spec.rb => http/http_spec.rb} (65%) rename spec/unit/{rest => http}/push_channel_spec.rb (70%) diff --git a/UPDATING.md b/UPDATING.md index 524548b7b..7e4b9b298 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -2,37 +2,110 @@ ## Version 1.x (`ably` gem) to 2.0.0 (`ably-pubsub-server` gem) -> **Status: draft.** The final public API naming is still under review; this section will be finalized before the 2.0.0 GA release. +### Gemfile -Version 2.0.0 splits the SDK into new packages. The `ably` gem is superseded: it receives security and critical-bug fixes only for one year from the 2.0.0 release date, and is then end-of-life. Under MAU-based pricing the platform must classify every connection as device- or server-side; the new packages declare this automatically, while the old constructors cannot — once MAU pricing is live, they raise on MAU-enabled accounts. +```ruby +# 1.x +gem 'ably' -Ruby is a server-side SDK, so there is a single new public gem, `ably-pubsub-server`, whose factory functions are the only entry points: the old constructors raise `Ably::Exceptions::DirectConstructionNotSupported`, because the package a client is created from is what declares its side and a directly constructed client declares none. +# 2.0 +gem 'ably-pubsub-server' +``` -### Mapping +### Require -| 1.x (`ably`) | 2.0 (`ably-pubsub-server`) | -| --- | --- | -| `gem 'ably'` | `gem 'ably-pubsub-server'` | -| `gem 'ably-rest'` (from `ably-ruby-rest`) | `gem 'ably-pubsub-server'` | -| `require 'ably'` | `require 'ably/pubsub/server'` | -| `Ably::Rest::Client.new(options)` | `Ably::PubSub::Server.create_http_client(options)` | -| `Ably::Rest.new(options)` | `Ably::PubSub::Server.create_http_client(options)` | -| `Ably::Realtime::Client.new(options)` | `Ably::PubSub::Server.create_realtime_client(options)` | -| `Ably::Realtime.new(options)` | `Ably::PubSub::Server.create_realtime_client(options)` | +```ruby +# 1.x +require 'ably' + +# 2.0 +require 'ably/pubsub/server' +``` -### Example +### HTTP client ```ruby # 1.x -require 'ably' client = Ably::Rest::Client.new(key: ENV['ABLY_API_KEY']) +client = Ably::Rest::Client.new('key.id:secret') +client = Ably::Rest::Client.new(token: 'token') +client = Ably::Rest.new(key: ENV['ABLY_API_KEY']) # 2.0 -require 'ably/pubsub/server' client = Ably::PubSub::Server.create_http_client(key: ENV['ABLY_API_KEY']) +client = Ably::PubSub::Server.create_http_client('key.id:secret') +client = Ably::PubSub::Server.create_http_client(token: 'token') ``` -Both factories accept everything the old constructors accepted: an options `Hash`, an API key `String`, or a token `String`. +### Realtime client + +```ruby +# 1.x +client = Ably::Realtime::Client.new(key: ENV['ABLY_API_KEY']) +client = Ably::Realtime::Client.new('key.id:secret') +client = Ably::Realtime.new(key: ENV['ABLY_API_KEY']) + +# 2.0 +client = Ably::PubSub::Server.create_realtime_client(key: ENV['ABLY_API_KEY']) +client = Ably::PubSub::Server.create_realtime_client('key.id:secret') +``` + +Both factories accept an options `Hash`, an API key `String`, or a token `String`. + +`Ably::PubSub::Http::Client.new` and `Ably::PubSub::Realtime::Client.new` raise +`Ably::Exceptions::DirectConstructionNotSupported`. + +### Namespaces + +| 1.x | 2.0 | +| --- | --- | +| `Ably::Rest::*` | `Ably::PubSub::Http::*` | +| `Ably::Realtime::*` | `Ably::PubSub::Realtime::*` | + +```ruby +# 1.x +client.is_a?(Ably::Rest::Client) +Ably::Rest::Client::MAX_MESSAGE_SIZE +Ably::Realtime::Channel::STATE.Attached +Ably::Realtime::Connection::STATE.Connected + +# 2.0 +client.is_a?(Ably::PubSub::Http::Client) +Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE +Ably::PubSub::Realtime::Channel::STATE.Attached +Ably::PubSub::Realtime::Connection::STATE.Connected +``` + +`Ably::Rest` and `Ably::Realtime` raise `NameError` in 2.0. + +### Unchanged + +Everything after construction: + +```ruby +client = Ably::PubSub::Server.create_http_client(key: ENV['ABLY_API_KEY']) + +channel = client.channels.get('example') +channel.publish 'event', 'payload' +channel.history +channel.presence.get + +client.auth.request_token +client.stats +client.time +``` + +```ruby +client = Ably::PubSub::Server.create_realtime_client(key: ENV['ABLY_API_KEY']) + +client.connection.on(:connected) { } + +channel = client.channels.get('example') +channel.attach +channel.subscribe { |message| } +channel.publish 'event', 'payload' +channel.presence.enter +``` ## Version 1.1.8 to 1.2.0 diff --git a/core/README.md b/core/README.md index 94c4c7966..f99eab792 100644 --- a/core/README.md +++ b/core/README.md @@ -4,7 +4,7 @@ Internal implementation package for Ably's own Pub/Sub Ruby packages. **This gem is not intended for direct external use.** It is published only so that Ably's public packages can depend on it, and it constructs nothing on its own: -`Ably::Rest::Client.new` and `Ably::Realtime::Client.new` raise +`Ably::PubSub::Http::Client.new` and `Ably::PubSub::Realtime::Client.new` raise `Ably::Exceptions::DirectConstructionNotSupported`. The package a client is created from is what declares its side to the platform, and a directly constructed client declares none. Ably's per-side packages construct clients through `Ably::Internal`, which is not diff --git a/core/lib/ably.rb b/core/lib/ably.rb index 3e1e96ff0..fba34d30c 100644 --- a/core/lib/ably.rb +++ b/core/lib/ably.rb @@ -10,10 +10,11 @@ end require 'ably/auth' +require 'ably/pubsub' require 'ably/exceptions' require 'ably/logger' -require 'ably/realtime' -require 'ably/rest' +require 'ably/pubsub/realtime' +require 'ably/pubsub/http' # Requires the client classes above: it constructs them. require 'ably/internal' diff --git a/core/lib/ably/auth.rb b/core/lib/ably/auth.rb index 764d0b09b..d0866c6b4 100644 --- a/core/lib/ably/auth.rb +++ b/core/lib/ably/auth.rb @@ -2,7 +2,7 @@ require 'faraday' require 'securerandom' -require 'ably/rest/middleware/external_exceptions' +require 'ably/pubsub/http/middleware/external_exceptions' module Ably # Creates Ably {Ably::Models::TokenRequest} objects and obtains Ably Tokens from Ably to subsequently issue to less trusted clients. @@ -45,7 +45,7 @@ class Auth # Creates an Auth object # - # @param [Ably::Rest::Client] client {Ably::Rest::Client} this Auth object uses + # @param [Ably::PubSub::Http::Client] client {Ably::PubSub::Http::Client} this Auth object uses # @param [Hash] token_params the token params used as a default for future token requests # @param [Hash] auth_options the authentication options used as a default future token requests # @option (see #request_token) @@ -759,7 +759,7 @@ def middleware setup_outgoing_middleware builder # Raise exceptions if response code is invalid - builder.use Ably::Rest::Middleware::ExternalExceptions + builder.use Ably::PubSub::Http::Middleware::ExternalExceptions setup_incoming_middleware builder, logger diff --git a/core/lib/ably/internal.rb b/core/lib/ably/internal.rb index 3e9f21f54..b82dca7d6 100644 --- a/core/lib/ably/internal.rb +++ b/core/lib/ably/internal.rb @@ -6,36 +6,36 @@ module Ably # change in any release, including a patch. Use the factory functions of the package you # installed — see {Ably::PubSub::Server} in the +ably-pubsub-server+ gem. # - # The client classes refuse direct construction ({Ably::Rest::Client.new} and - # {Ably::Realtime::Client.new} raise {Ably::Exceptions::DirectConstructionNotSupported}), + # The client classes refuse direct construction ({Ably::PubSub::Http::Client.new} and + # {Ably::PubSub::Realtime::Client.new} raise {Ably::Exceptions::DirectConstructionNotSupported}), # because the package a client is created from is what declares the client's side to the # platform and a directly constructed client declares none. These entry points are how a # per-side package constructs the client it has stamped its side onto. module Internal class << self - # Constructs an {Ably::Rest::Client}, bypassing the direct-construction refusal. + # Constructs an {Ably::PubSub::Http::Client}, bypassing the direct-construction refusal. # - # @param options [Hash, String] as accepted by {Ably::Rest::Client#initialize} - # @return [Ably::Rest::Client] + # @param options [Hash, String] as accepted by {Ably::PubSub::Http::Client#initialize} + # @return [Ably::PubSub::Http::Client] # @api private - def create_rest_client(options) - construct(Ably::Rest::Client, options) + def create_http_client(options) + construct(Ably::PubSub::Http::Client, options) end - # Constructs an {Ably::Realtime::Client}, bypassing the direct-construction refusal. + # Constructs an {Ably::PubSub::Realtime::Client}, bypassing the direct-construction refusal. # - # @param options [Hash, String] as accepted by {Ably::Realtime::Client#initialize} - # @return [Ably::Realtime::Client] + # @param options [Hash, String] as accepted by {Ably::PubSub::Realtime::Client#initialize} + # @return [Ably::PubSub::Realtime::Client] # @api private def create_realtime_client(options) - construct(Ably::Realtime::Client, options) + construct(Ably::PubSub::Realtime::Client, options) end # Builds the error raised when a client is constructed directly. Shared so the - # refusals on the client classes and on the Ably::Rest / Ably::Realtime convenience - # aliases all speak with one voice. + # refusals on the client classes and on the Ably::PubSub::Http / + # Ably::PubSub::Realtime convenience aliases all speak with one voice. # - # @param subject [String] the unsupported call, e.g. +"Ably::Rest::Client.new"+ + # @param subject [String] the unsupported call, e.g. +"Ably::PubSub::Http::Client.new"+ # @param factory [String] the supported call to use instead # @return [Ably::Exceptions::DirectConstructionNotSupported] # @api private diff --git a/core/lib/ably/logger.rb b/core/lib/ably/logger.rb index 0c705510c..035b0dbff 100644 --- a/core/lib/ably/logger.rb +++ b/core/lib/ably/logger.rb @@ -1,12 +1,12 @@ module Ably # Logger unifies logging for #debug, #info, #warn, #error, and #fatal messages. # A new Ably client uses this Logger and sets the appropriate log level. - # A custom Logger can be configured when instantiating the client, refer to the {Ably::Rest::Client} and {Ably::Realtime::Client} documentation + # A custom Logger can be configured when instantiating the client, refer to the {Ably::PubSub::Http::Client} and {Ably::PubSub::Realtime::Client} documentation # class Logger extend Forwardable - # @param client [Ably::Rest::Client,Ably::Realtime::Client] Rest or Realtime Ably client + # @param client [Ably::PubSub::Http::Client,Ably::PubSub::Realtime::Client] Rest or Realtime Ably client # @param log_level [Integer] {http://www.ruby-doc.org/stdlib-3.1.1/libdoc/logger/rdoc/Logger.html Ruby Logger} log level # @param custom_logger [nil,Object] A custom logger can optionally be used instead of the, # however it must provide a {http://www.ruby-doc.org/stdlib-3.1.1/libdoc/logger/rdoc/Logger.html Ruby Logger} compatible interface. @@ -83,7 +83,7 @@ def connection_id end def realtime? - defined?(Ably::Realtime::Client) && client.kind_of?(Ably::Realtime::Client) + defined?(Ably::PubSub::Realtime::Client) && client.kind_of?(Ably::PubSub::Realtime::Client) end def default_logger diff --git a/core/lib/ably/models/channel_details.rb b/core/lib/ably/models/channel_details.rb index 268bd5588..03e75f63a 100644 --- a/core/lib/ably/models/channel_details.rb +++ b/core/lib/ably/models/channel_details.rb @@ -14,7 +14,7 @@ def self.ChannelDetails(attributes) end end - # Contains the details of a {Ably::Models::Rest::Channel} or {Ably::Models::Realtime::Channel} object + # Contains the details of a {Ably::PubSub::Http::Channel} or {Ably::PubSub::Realtime::Channel} object # such as its ID and {Ably::Models::ChannelStatus}. # class ChannelDetails diff --git a/core/lib/ably/models/channel_metrics.rb b/core/lib/ably/models/channel_metrics.rb index 05f57ad83..35ab2935e 100644 --- a/core/lib/ably/models/channel_metrics.rb +++ b/core/lib/ably/models/channel_metrics.rb @@ -14,7 +14,7 @@ def self.ChannelMetrics(attributes) end end - # Contains the metrics associated with a {Ably::Models::Rest::Channel} or {Ably::Models::Realtime::Channel}, + # Contains the metrics associated with a {Ably::PubSub::Http::Channel} or {Ably::PubSub::Realtime::Channel}, # such as the number of publishers, subscribers and connections it has. # # @spec CHM1 diff --git a/core/lib/ably/models/channel_occupancy.rb b/core/lib/ably/models/channel_occupancy.rb index eef9e51aa..d06241b8e 100644 --- a/core/lib/ably/models/channel_occupancy.rb +++ b/core/lib/ably/models/channel_occupancy.rb @@ -14,7 +14,7 @@ def self.ChannelOccupancy(attributes) end end - # Contains the metrics of a {Ably::Models::Rest::Channel} or {Ably::Models::Realtime::Channel} object. + # Contains the metrics of a {Ably::PubSub::Http::Channel} or {Ably::PubSub::Realtime::Channel} object. # # @spec CHO1 # diff --git a/core/lib/ably/models/channel_state_change.rb b/core/lib/ably/models/channel_state_change.rb index 6c843939b..cff5137ee 100644 --- a/core/lib/ably/models/channel_state_change.rb +++ b/core/lib/ably/models/channel_state_change.rb @@ -1,5 +1,5 @@ module Ably::Models - # Contains state change information emitted by {Ably::Rest::Channel} and {Ably::Realtime::Channel} objects. + # Contains state change information emitted by {Ably::PubSub::Http::Channel} and {Ably::PubSub::Realtime::Channel} objects. # class ChannelStateChange include Ably::Modules::ModelCommon @@ -21,31 +21,31 @@ def initialize(hash_object) raise ArgumentError, e end - # The new current {Ably::Realtime::Channel::STATE}. + # The new current {Ably::PubSub::Realtime::Channel::STATE}. # # @spec RTL2a, RTL2b # - # @return [Ably::Realtime::Channel::STATE] + # @return [Ably::PubSub::Realtime::Channel::STATE] # def current @hash_object[:current] end - # The previous state. For the {Ably::Realtime::Channel::EVENT}(:update) event, this is equal to the current {Ably::Realtime::Channel::STATE}. + # The previous state. For the {Ably::PubSub::Realtime::Channel::EVENT}(:update) event, this is equal to the current {Ably::PubSub::Realtime::Channel::STATE}. # # @spec RTL2a, RTL2b # - # @return [Ably::Realtime::Channel::EVENT] + # @return [Ably::PubSub::Realtime::Channel::EVENT] # def previous @hash_object[:previous] end - # The event that triggered this {Ably::Realtime::Channel::STATE} change. + # The event that triggered this {Ably::PubSub::Realtime::Channel::STATE} change. # # @spec TH5 # - # @return [Ably::Realtime::Channel::STATE] + # @return [Ably::PubSub::Realtime::Channel::STATE] # def event @hash_object[:event] diff --git a/core/lib/ably/models/channel_status.rb b/core/lib/ably/models/channel_status.rb index 175d23371..8205cee02 100644 --- a/core/lib/ably/models/channel_status.rb +++ b/core/lib/ably/models/channel_status.rb @@ -14,7 +14,7 @@ def self.ChannelStatus(attributes) end end - # Contains the status of a {Ably::Models::Rest::Channel} or {Ably::Models::Realtime::Channel} object + # Contains the status of a {Ably::PubSub::Http::Channel} or {Ably::PubSub::Realtime::Channel} object # such as whether it is active and its {Ably::Models::ChannelOccupancy}. # # @spec CHS1 diff --git a/core/lib/ably/models/cipher_params.rb b/core/lib/ably/models/cipher_params.rb index d9ba6a8ee..4278dfaa1 100644 --- a/core/lib/ably/models/cipher_params.rb +++ b/core/lib/ably/models/cipher_params.rb @@ -17,7 +17,7 @@ def self.CipherParams(attributes) end end - # Sets the properties to configure encryption for a {Ably::Models::Rest::Channel} or {Ably::Models::Realtime::Channel} object. + # Sets the properties to configure encryption for a {Ably::PubSub::Http::Channel} or {Ably::PubSub::Realtime::Channel} object. # class CipherParams include Ably::Modules::ModelCommon diff --git a/core/lib/ably/models/connection_details.rb b/core/lib/ably/models/connection_details.rb index 06f87e94a..e1b586867 100644 --- a/core/lib/ably/models/connection_details.rb +++ b/core/lib/ably/models/connection_details.rb @@ -14,8 +14,8 @@ def self.ConnectionDetails(attributes) end end - # Contains any constraints a client should adhere to and provides additional metadata about a {Ably::Realtime::Connection}, - # such as if a request to {Ably::Realtime::Client#publish} a message that exceeds the maximum message size should be rejected immediately without communicating with Ably. + # Contains any constraints a client should adhere to and provides additional metadata about a {Ably::PubSub::Realtime::Connection}, + # such as if a request to {Ably::PubSub::Realtime::Client#publish} a message that exceeds the maximum message size should be rejected immediately without communicating with Ably. # class ConnectionDetails include Ably::Modules::ModelCommon diff --git a/core/lib/ably/models/connection_state_change.rb b/core/lib/ably/models/connection_state_change.rb index e7f609cc6..90e01c175 100644 --- a/core/lib/ably/models/connection_state_change.rb +++ b/core/lib/ably/models/connection_state_change.rb @@ -1,5 +1,5 @@ module Ably::Models - # Contains {Ably::Models::ConnectionState} change information emitted by the {Ably::Realtime::Connection} object. + # Contains {Ably::Models::ConnectionState} change information emitted by the {Ably::PubSub::Realtime::Connection} object. # class ConnectionStateChange include Ably::Modules::ModelCommon @@ -21,21 +21,21 @@ def initialize(hash_object) raise ArgumentError, e end - # The new {Ably::Realtime::Connection::STATE}. + # The new {Ably::PubSub::Realtime::Connection::STATE}. # # @spec TA2 # - # @return [Ably::Realtime::Connection::STATE] + # @return [Ably::PubSub::Realtime::Connection::STATE] # def current @hash_object[:current] end - # The event that triggered this {Ably::Realtime::Connection::EVENT} change. + # The event that triggered this {Ably::PubSub::Realtime::Connection::EVENT} change. # # @spec TA5 # - # @return [Ably::Realtime::Connection::STATE] + # @return [Ably::PubSub::Realtime::Connection::STATE] # def event @hash_object[:event] @@ -46,7 +46,7 @@ def event # # @spec TA2 # - # @return [Ably::Realtime::Connection::STATE] + # @return [Ably::PubSub::Realtime::Connection::STATE] # def previous @hash_object[:previous] diff --git a/core/lib/ably/models/http_paginated_response.rb b/core/lib/ably/models/http_paginated_response.rb index 052867694..2231370ba 100644 --- a/core/lib/ably/models/http_paginated_response.rb +++ b/core/lib/ably/models/http_paginated_response.rb @@ -1,13 +1,13 @@ require 'ably/models/paginated_result' module Ably::Models - # HTTP respones object from Rest#request object + # HTTP response object from Ably::PubSub::Http::Client#request # Wraps any Ably HTTP response that supports paging and provides methods to iterate through # the pages using {#first}, {#next}, {#has_next?} and {#last?} class HttpPaginatedResponse < PaginatedResult # Retrieve the first page of results. - # When used as part of the {Ably::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object, + # When used as part of the {Ably::PubSub::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object, # and allows an optional success callback block to be provided. # # @return [HttpPaginatedResponse,Ably::Util::SafeDeferrable] @@ -20,7 +20,7 @@ def first(&success_callback) end # Retrieve the next page of results. - # When used as part of the {Ably::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object, + # When used as part of the {Ably::PubSub::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object, # and allows an optional success callback block to be provided. # # @return [HttpPaginatedResponse,Ably::Util::SafeDeferrable] diff --git a/core/lib/ably/models/message.rb b/core/lib/ably/models/message.rb index 5c5817b06..f1d520d7e 100644 --- a/core/lib/ably/models/message.rb +++ b/core/lib/ably/models/message.rb @@ -25,7 +25,7 @@ class Message include Ably::Modules::Conversions include Ably::Modules::Encodeable include Ably::Modules::ModelCommon - include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime) + include Ably::Modules::SafeDeferrable if defined?(Ably::PubSub::Realtime) # Statically register a default set of encoders for this class Ably::Models::MessageEncoders.register_default_encoders self diff --git a/core/lib/ably/models/paginated_result.rb b/core/lib/ably/models/paginated_result.rb index 47b8c5d24..6d4fce691 100644 --- a/core/lib/ably/models/paginated_result.rb +++ b/core/lib/ably/models/paginated_result.rb @@ -4,7 +4,7 @@ module Ably::Models # that indicates the relative queries available to the PaginatedResult object. # class PaginatedResult - include Ably::Modules::AsyncWrapper if defined?(Ably::Realtime) + include Ably::Modules::AsyncWrapper if defined?(Ably::PubSub::Realtime) # Contains the current page of results; for example, an array of {Ably::Models::Message} or {Ably::Models::PresenceMessage} objects # for a channel history request. @@ -59,7 +59,7 @@ def first(&success_callback) # Retrieve the next page of results. # - # When used as part of the {Ably::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object, + # When used as part of the {Ably::PubSub::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object, # and allows an optional success callback block to be provided. # # @spec TG4 diff --git a/core/lib/ably/models/presence_message.rb b/core/lib/ably/models/presence_message.rb index f30582f52..8f0dab624 100644 --- a/core/lib/ably/models/presence_message.rb +++ b/core/lib/ably/models/presence_message.rb @@ -24,7 +24,7 @@ class PresenceMessage include Ably::Modules::Conversions include Ably::Modules::Encodeable include Ably::Modules::ModelCommon - include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime) + include Ably::Modules::SafeDeferrable if defined?(Ably::PubSub::Realtime) extend Ably::Modules::Enum # Describes the possible actions members in the presence set can emit. diff --git a/core/lib/ably/models/protocol_message.rb b/core/lib/ably/models/protocol_message.rb index 19ac1662e..dc2a277c4 100644 --- a/core/lib/ably/models/protocol_message.rb +++ b/core/lib/ably/models/protocol_message.rb @@ -36,7 +36,7 @@ module Ably::Models class ProtocolMessage include Ably::Modules::ModelCommon include Ably::Modules::Encodeable - include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime) + include Ably::Modules::SafeDeferrable if defined?(Ably::PubSub::Realtime) extend Ably::Modules::Enum # Actions which are sent by the Ably Realtime API diff --git a/core/lib/ably/modules/ably.rb b/core/lib/ably/modules/ably.rb index 0cb5ef9ac..b566588e3 100644 --- a/core/lib/ably/modules/ably.rb +++ b/core/lib/ably/modules/ably.rb @@ -1,4 +1,4 @@ -# Ably is the base namespace for the Ably {Ably::Realtime Realtime} & {Ably::Rest Rest} client libraries. +# Ably is the base namespace for the Ably {Ably::PubSub::Realtime Realtime} & {Ably::PubSub::Http Rest} client libraries. # # Please refer to the {file:README.md Readme} on getting started. # diff --git a/core/lib/ably/modules/channels_collection.rb b/core/lib/ably/modules/channels_collection.rb index aa9c14214..29ccf8072 100644 --- a/core/lib/ably/modules/channels_collection.rb +++ b/core/lib/ably/modules/channels_collection.rb @@ -1,5 +1,5 @@ module Ably::Modules - # ChannelsCollection module provides common functionality to the Rest and Realtime Channels objects + # ChannelsCollection module provides common functionality to the HTTP and Realtime Channels objects # such as #get, #[], #fetch, and #release module ChannelsCollection include Enumerable diff --git a/core/lib/ably/modules/http_helpers.rb b/core/lib/ably/modules/http_helpers.rb index 82a933b7b..40cdfe55e 100644 --- a/core/lib/ably/modules/http_helpers.rb +++ b/core/lib/ably/modules/http_helpers.rb @@ -2,12 +2,12 @@ require 'ably/version' -require 'ably/rest/middleware/encoder' -require 'ably/rest/middleware/external_exceptions' -require 'ably/rest/middleware/fail_if_unsupported_mime_type' -require 'ably/rest/middleware/logger' -require 'ably/rest/middleware/parse_json' -require 'ably/rest/middleware/parse_message_pack' +require 'ably/pubsub/http/middleware/encoder' +require 'ably/pubsub/http/middleware/external_exceptions' +require 'ably/pubsub/http/middleware/fail_if_unsupported_mime_type' +require 'ably/pubsub/http/middleware/logger' +require 'ably/pubsub/http/middleware/parse_json' +require 'ably/pubsub/http/middleware/parse_message_pack' module Ably::Modules # HttpHelpers provides common private methods to classes to simplify HTTP interactions with Ably @@ -23,19 +23,19 @@ def user_agent def setup_outgoing_middleware(builder) # Convert request params to "www-form-urlencoded" - builder.use Ably::Rest::Middleware::Encoder + builder.use Ably::PubSub::Http::Middleware::Encoder end def setup_incoming_middleware(builder, logger, options = {}) - builder.use Ably::Rest::Middleware::Logger, logger + builder.use Ably::PubSub::Http::Middleware::Logger, logger # Parse JSON / MsgPack response bodies. ParseJson must be first (default) parsing middleware if options[:fail_if_unsupported_mime_type] == true - builder.use Ably::Rest::Middleware::FailIfUnsupportedMimeType + builder.use Ably::PubSub::Http::Middleware::FailIfUnsupportedMimeType end - builder.use Ably::Rest::Middleware::ParseJson - builder.use Ably::Rest::Middleware::ParseMessagePack + builder.use Ably::PubSub::Http::Middleware::ParseJson + builder.use Ably::PubSub::Http::Middleware::ParseMessagePack end end end diff --git a/core/lib/ably/pubsub.rb b/core/lib/ably/pubsub.rb new file mode 100644 index 000000000..ac36cc49a --- /dev/null +++ b/core/lib/ably/pubsub.rb @@ -0,0 +1,11 @@ +module Ably + # The namespace of Ably's Pub/Sub packages. The implementation lives here + # (+Ably::PubSub::Http+, +Ably::PubSub::Realtime+ and their models), and each per-side + # package adds its own factory module alongside — +Ably::PubSub::Server+ from the + # +ably-pubsub-server+ gem. + # + # This file only opens the namespace. Files that declare into it with the compact form + # (+module Ably::PubSub::Http+) need it to exist already, so it is required first. + module PubSub + end +end diff --git a/core/lib/ably/pubsub/http.rb b/core/lib/ably/pubsub/http.rb new file mode 100644 index 000000000..c6d626ee0 --- /dev/null +++ b/core/lib/ably/pubsub/http.rb @@ -0,0 +1,38 @@ +require 'ably/pubsub/http/channel' +require 'ably/pubsub/http/channels' +require 'ably/pubsub/http/client' +require 'ably/pubsub/http/push' +require 'ably/pubsub/http/presence' + +require 'ably/models/message_encoders/base' + +Dir.glob(File.expand_path("../models/*.rb", File.dirname(__FILE__))).each do |file| + require file +end + +module Ably + module PubSub + # Http is the namespace of the stateless HTTP client and the models it returns. + # + # @example + # client = Ably::PubSub::Server.create_http_client("xxxxx") + # channel = client.channel("test") + # channel.publish "greeting", "data" + # + module Http + # Refuses construction. This was a convenience alias for the {Ably::PubSub::Http::Client} + # constructor, which no longer accepts direct construction: the package a client is + # created from is what declares the client's side to the platform, and a client + # constructed here declares none. + # + # Use {Ably::PubSub::Server.create_http_client} from the +ably-pubsub-server+ gem. + # + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::PubSub::Http.new', 'Ably::PubSub::Server.create_http_client(options)' + ) + end + end + end +end diff --git a/core/lib/ably/pubsub/http/channel.rb b/core/lib/ably/pubsub/http/channel.rb new file mode 100644 index 000000000..f483ee608 --- /dev/null +++ b/core/lib/ably/pubsub/http/channel.rb @@ -0,0 +1,196 @@ +module Ably + module PubSub + module Http + # Enables messages to be published and historic messages to be retrieved for a channel. + # + class Channel + include Ably::Modules::Conversions + + # Ably client associated with this channel + # @return [Ably::PubSub::Realtime::Client] + # @api private + attr_reader :client + + # The channel name. + # @return [String] + attr_reader :name + + attr_reader :options + + # A {Ably::PubSub::Http::Channel::PushChannel} object + # @spec RSH4 + # @return [Ably::PubSub::Http::Channel::PushChannel] + # @api private + attr_reader :push + + IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH = 9 # See spec RSL1k1 + + # Initialize a new Channel object + # + # @param client [Ably::PubSub::Http::Client] + # @param name [String] The name of the channel + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} + # + def initialize(client, name, channel_options = {}) + name = (ensure_utf_8 :name, name) + + @options = Ably::Models::ChannelOptions(channel_options) + @client = client + @name = name + @push = PushChannel.new(self) + end + + # Publishes a message to the channel. A callback may optionally be passed in to this call to be notified of success or failure of the operation. + # + # @spec RSL1 + # + # @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 + # + # @example + # # Publish a single message with (name, data) form + # channel.publish 'click', { x: 1, y: 2 } + # + # # Publish a single message with single Hash form + # message = { name: 'click', data: { x: 1, y: 2 } } + # channel.publish message + # + # # Publish an array of message Hashes form + # messages = [ + # { name: 'click', data: { x: 1, y: 2 } }, + # { name: 'click', data: { x: 2, y: 3 } } + # ] + # channel.publish messages + # + # # Publish an array of Ably::Models::Message objects form + # messages = [ + # Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) + # Ably::Models::Message(name: 'click', data: { x: 2, y: 3 }) + # ] + # channel.publish messages + # + # # Publish a single Ably::Models::Message object form + # message = Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) + # channel.publish message + # + def publish(name, data = nil, attributes = {}) + qs_params = nil + qs_params = data if name.kind_of?(Enumerable) || name.kind_of?(Ably::Models::Message) + + messages = build_messages(name, data, attributes) # (RSL1a, RSL1b) + + if messages.sum(&:size) > (max_message_size = client.max_message_size || Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE) + raise Ably::Exceptions::MaxMessageSizeExceeded.new("Maximum message size exceeded #{max_message_size} bytes.") + end + + payload = messages.map do |message| + Ably::Models::Message(message.dup).tap do |msg| + msg.encode client.encoders, options + + next if msg.client_id.nil? + if msg.client_id == '*' + raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages') + end + unless client.auth.can_assume_client_id?(msg.client_id) + raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{msg.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'") + end + end.as_json + end.tap do |payload| + if client.idempotent_rest_publishing + # We cannot mutate for idempotent publishing if one or more messages already has an ID + if payload.all? { |msg| !msg['id'] } + # Mutate the JSON to support idempotent publishing where a Message.id does not exist + idempotent_publish_id = SecureRandom.base64(IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH) + payload.each_with_index do |msg, idx| + msg['id'] = "#{idempotent_publish_id}:#{idx}" + end + end + end + end + + 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) + end + + # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical {Ably::Models::Message} + # objects for the channel. If the channel is configured to persist messages, then messages can be retrieved from + # history for up to 72 hours in the past. If not, messages can only be retrieved from history for up to two minutes in the past. + # + # @spec RSL2a + # + # @param [Hash] options the options for the message history request + # @option options [Integer,Time] :start The time from which messages are retrieved, specified as milliseconds since the Unix epoch. RSL2b1 + # @option options [Integer,Time] :end The time until messages are retrieved, specified as milliseconds since the Unix epoch. RSL2b1 + # @option options [Symbol] :direction The order for which messages are returned in. Valid values are backwards which orders messages from most recent to oldest, or forwards which orders messages from oldest to most recent. The default is backwards. RSL2b2 + # @option options [Integer] :limit An upper limit on the number of messages returned. The default is 100, and the maximum is 1000. RSL2b3 + # + # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::Message} objects. + # + def history(options = {}) + url = "#{base_path}/messages" + options = { + :direction => :backwards, + :limit => 100 + }.merge(options) + + [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) } + raise ArgumentError, ":end must be equal to or after :start" if options[:start] && options[:end] && (options[:start] > options[:end]) + + paginated_options = { + coerce_into: 'Ably::Models::Message', + async_blocking_operations: options.delete(:async_blocking_operations), + } + + response = client.get(url, options) + + Ably::Models::PaginatedResult.new(response, url, client, paginated_options) do |message| + message.tap do |msg| + decode_message msg + end + end + end + + # A {Ably::PubSub::Http::Presence} object. + # @spec RSL3 + # @return [Ably::PubSub::Http::Presence] + def presence + @presence ||= Presence.new(client, self) + end + + # Sets the {Ably::Models::ChannelOptions} for the channel. + # @spec RSL7 + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} + # @return [Ably::Models::ChannelOptions] + def set_options(channel_options) + @options = Ably::Models::ChannelOptions(channel_options) + end + alias options= set_options + + # Retrieves a {Ably::Models::ChannelDetails} object for the channel, which includes status and occupancy metrics. + # @spec RSL8 + # @return [Ably::Models::ChannelDetails] A {Ably::Models::ChannelDetails} object. + def status + Ably::Models::ChannelDetails.new(client.get(base_path).body) + end + + private + + def base_path + "/channels/#{URI.encode_www_form_component(name)}" + end + + def decode_message(message) + message.decode client.encoders, options + rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e + client.logger.error { "Decoding Error on channel '#{name}', message event name '#{message.name}'. #{e.class.name}: #{e.message}" } + end + end + end + end +end + +require 'ably/pubsub/http/channel/push_channel' diff --git a/core/lib/ably/rest/channel/push_channel.rb b/core/lib/ably/pubsub/http/channel/push_channel.rb similarity index 92% rename from core/lib/ably/rest/channel/push_channel.rb rename to core/lib/ably/pubsub/http/channel/push_channel.rb index 0c3e3b5b8..17f6e7295 100644 --- a/core/lib/ably/rest/channel/push_channel.rb +++ b/core/lib/ably/pubsub/http/channel/push_channel.rb @@ -1,13 +1,13 @@ -module Ably::Rest +module Ably::PubSub::Http class Channel # A push channel used for push notifications - # Each PushChannel maps to exactly one Rest Channel + # Each PushChannel maps to exactly one HTTP Channel # class PushChannel attr_reader :channel def initialize(channel) - raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::Rest::Channel) + raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::PubSub::Http::Channel) @channel = channel end diff --git a/core/lib/ably/pubsub/http/channels.rb b/core/lib/ably/pubsub/http/channels.rb new file mode 100644 index 000000000..a47a15e29 --- /dev/null +++ b/core/lib/ably/pubsub/http/channels.rb @@ -0,0 +1,49 @@ +module Ably + module PubSub + module Http + class Channels + include Ably::Modules::ChannelsCollection + + # @return [Ably::PubSub::Http::Channels] + def initialize(client) + super client, Ably::PubSub::Http::Channel + end + + # Return a {Ably::PubSub::Http::Channel} for the given name + # + # @param name [String] The name of the channel + # @param channel_options [Hash] Channel options, currently reserved for Encryption options + # + # @return [Ably::PubSub::Http::Channel] + # + def get(*args) + super + end + + # Return a {Ably::PubSub::Http::Channel} for the given name if it exists, else the block will be called. + # This method is intentionally similar to {http://ruby-doc.org/core-2.1.3/Hash.html#method-i-fetch Hash#fetch} providing a simple way to check if a channel exists or not without creating one + # + # @param name [String] The name of the channel + # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called + # @yieldparam [String] name of the missing channel + # + # @return [Ably::PubSub::Http::Channel] + # + def fetch(*args) + super + end + + # Destroy the {Ably::PubSub::Http::Channel} and releases the associated resources. + # + # Releasing a {Ably::PubSub::Http::Channel} is not typically necessary as a channel consumes no resources other than the memory footprint of the + # {Ably::PubSub::Http::Channel} object. Explicitly release channels to free up resources if required + # + # @return [void] + # + def release(*args) + super + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/client.rb b/core/lib/ably/pubsub/http/client.rb new file mode 100644 index 000000000..7f49dbd8d --- /dev/null +++ b/core/lib/ably/pubsub/http/client.rb @@ -0,0 +1,743 @@ +require 'faraday' +require 'json' +require 'logger' +require 'uri' + +require 'typhoeus' +require 'faraday/typhoeus' + +require 'ably/pubsub/http/middleware/exceptions' + +module Ably + module PubSub + module Http + # A client that offers a simple stateless API to interact directly with Ably's REST API. + # + class Client + include Ably::Modules::Conversions + include Ably::Modules::HttpHelpers + extend Forwardable + using Ably::Util::AblyExtensions + + # Default Ably domain for REST + DOMAIN = 'rest.ably.io' + + MAX_MESSAGE_SIZE = 65536 # See spec TO3l8 + MAX_FRAME_SIZE = 524288 # See spec TO3l8 + + # Configuration for HTTP timeouts and HTTP request reattempts to fallback hosts + HTTP_DEFAULTS = { + open_timeout: 4, + request_timeout: 10, + max_retry_duration: 15, + max_retry_count: 3 + }.freeze + + FALLBACK_RETRY_TIMEOUT = 10 * 60 + + # Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError + FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError) + [Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError] + else + Faraday::ClientError + end + + def_delegators :auth, :client_id, :auth_options + + # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment + # @return [String] + attr_reader :environment + + # The protocol configured for this client, either binary `:msgpack` or text based `:json` + # @return [Symbol] + attr_reader :protocol + + # Client agent i.e. `example-gem/1.2.0 ably-pubsub-ruby/1.1.5 ruby/3.1.1` + # @return [String] + attr_reader :agent + + # An {Ably::Auth} object. + # @spec RSC5 + # @return [Ably::Auth] + attr_reader :auth + + # A {Ably::PubSub::Http::Channels} object. + # @spec RSN1 + # @return [Ably::PubSub::Http::Channels] + attr_reader :channels + + # Log level configured for this {Client} + # @return [Logger::Severity] + attr_reader :log_level + + # The custom host that is being used if it was provided with the option +:rest_host+ when the {Client} was created + # @return [String,Nil] + attr_reader :custom_host + + # The custom port for non-TLS requests if it was provided with the option +:port+ when the {Client} was created + # @return [Integer,Nil] + attr_reader :custom_port + + # The custom TLS port for TLS requests if it was provided with the option +:tls_port+ when the {Client} was created + # @return [Integer,Nil] + attr_reader :custom_tls_port + + # The immutable configured HTTP defaults for this client. + # See {#initialize} for the configurable HTTP defaults prefixed with +http_+ + # @return [Hash] + attr_reader :http_defaults + + # The registered encoders that are used to encode and decode message payloads + # @return [Array] + # @api private + attr_reader :encoders + + # The additional options passed to this Client's #initialize method not available as attributes of this class + # @return [Hash] + # @api private + attr_reader :options + + # The list of fallback hosts to be used by this client + # if empty or nil then fallback host functionality is disabled + attr_reader :fallback_hosts + + # Whether the {Client} has to add a random identifier to the path of a request + # @return [Boolean] + attr_reader :add_request_ids + + # Retries are logged by default to warn and error. When true, retries are logged at info level + # @return [Boolean] + # @api private + attr_reader :log_retries_as_info + + # True when idempotent publishing is enabled for all messages published via REST. + # When this feature is enabled, the client library will add a unique ID to every published message (without an ID) + # ensuring any failed published attempts (due to failures such as HTTP requests failing mid-flight) that are + # automatically retried will not result in duplicate messages being published to the Ably platform. + # Note: This is a beta unsupported feature! + # @return [Boolean] + attr_reader :idempotent_rest_publishing + + # Max message size (TO2, TO3l8) by default (65536 bytes) 64KiB + # @return [Integer] + attr_reader :max_message_size + + # Max frame size (TO2, TO3l8) by default (524288 bytes) 512KiB + # @return [Integer] + attr_reader :max_frame_size + + # Refuses direct construction: the package a client is created from is what declares + # the client's side to the platform, and a directly constructed client declares none. + # Use {Ably::PubSub::Server.create_http_client} from the +ably-pubsub-server+ gem. + # + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::PubSub::Http::Client.new', 'Ably::PubSub::Server.create_http_client(options)' + ) + end + + # Constructs a {Ably::PubSub::Http::Client} object using an Ably API key or token string. + # + # @spec RSC1 + # + # @param [Hash,String] options an options Hash or String used to configure the client and the authentication, or String with an API key or Token ID + # @option options [Boolean] :tls (true) When false, TLS is disabled. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections. + # @option options [String] :key API key comprising the key name and key secret in a single string + # @option options [String] :token Token string or {Models::TokenDetails} used to authenticate requests + # @option options [String] :token_details {Models::TokenDetails} used to authenticate requests + # @option options [Boolean] :use_token_auth Will force Basic Auth if set to false, and Token auth if set to true + # @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment + # @option options [Symbol] :protocol (:msgpack) Protocol used to communicate with Ably, :json and :msgpack currently supported + # @option options [Boolean] :use_binary_protocol (true) When true will use the MessagePack binary protocol, when false it will use JSON encoding. This option will overide :protocol option + # @option options [Logger::Severity,Symbol] :log_level (Logger::WARN) Log level for the standard Logger that outputs to STDOUT. Can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none + # @option options [Logger] :logger A custom logger can be used however it must adhere to the Ruby Logger interface, see http://www.ruby-doc.org/stdlib-3.1.1/libdoc/logger/rdoc/Logger.html + # @option options [String] :client_id client ID identifying this connection to other clients + # @option options [String] :auth_url a URL to be used to GET or POST a set of token request params, to obtain a signed token request + # @option options [Hash] :auth_headers a set of application-specific headers to be added to any request made to the +auth_url+ + # @option options [Hash] :auth_params a set of application-specific query params to be added to any request made to the +auth_url+ + # @option options [Symbol] :auth_method (:get) HTTP method to use with +auth_url+, must be either +:get+ or +:post+ + # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required. + # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent + # @option options [Boolean] :query_time when true will query the {https://www.ably.com Ably} system for the current time instead of using the local time + # @option options [Hash] :default_token_params convenience to pass in +token_params+ that will be used as a default for all token requests. See {Auth#create_token_request} + # + # @option options [Integer] :http_open_timeout (4 seconds) timeout in seconds for opening an HTTP connection for all HTTP requests + # @option options [Integer] :http_request_timeout (10 seconds) timeout in seconds for any single complete HTTP request and response + # @option options [Integer] :http_max_retry_count (3) maximum number of fallback host retries for HTTP requests that fail due to network issues or server problems + # @option options [Integer] :http_max_retry_duration (15 seconds) maximum elapsed time in which fallback host retries for HTTP requests will be attempted i.e. if the first default host attempt takes 5s, and then the subsequent fallback retry attempt takes 7s, no further fallback host attempts will be made as the total elapsed time of 12s exceeds the default 10s limit + # + # @option options [Boolean] :fallback_hosts_use_default (false) When true, forces the user of fallback hosts even if a non-default production endpoint is being used + # @option options [Array] :fallback_hosts When an array of fallback hosts are provided, these fallback hosts are always used if a request fails to the primary endpoint. If an empty array is provided, the fallback host functionality is disabled + # @option options [Integer] :fallback_retry_timeout (600 seconds) amount of time in seconds a REST client will continue to use a working fallback host when the primary fallback host has previously failed + # + # @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request. + # @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST + # @option options [Integer] :max_message_size (65536 bytes) Maximum size of all messages when publishing via REST publish() + # @option options [Integer] :max_frame_size (524288 bytes) Maximum size of frame + # + # @return [Ably::PubSub::Http::Client] + # + # @example + # # Create a new client authenticating with basic auth using a String object + # client = Ably::PubSub::Server.create_http_client('key.id:secret') + # + # # Construct a client using a Hash object. + # client = Ably::PubSub::Server.create_http_client(key: 'key.id:secret', client_id: 'john') + # + # Applications do not call this constructor: {.new} refuses direct construction, and + # a per-side package reaches it through {Ably::Internal.create_http_client}. + # + def initialize(options) + raise ArgumentError, 'Options Hash is expected' if options.nil? + + options = options.clone + if options.kind_of?(String) + options = if options.match(Auth::API_KEY_REGEX) + { key: options } + else + { token: options } + end + end + + @agent = options.delete(:agent) || Ably::AGENT + # Additive agent entries (`identifier => version`), appended to the base agent + # string. This is how a package layered on this one (such as ably-pubsub-server) + # declares itself — including the side-declaring entry that MAU classification + # reads — without replacing the base identifiers the way :agent does. + options.delete(:agents).to_h.each do |identifier, version| + @agent = "#{@agent} #{version ? "#{identifier}/#{version}" : identifier}" + end + @realtime_client = options.delete(:realtime_client) + @tls = options.delete_with_default(:tls, true) + @environment = options.delete(:environment) # nil is production + @environment = nil if [:production, 'production'].include?(@environment) + @protocol = options.delete(:protocol) || :msgpack + @debug_http = options.delete(:debug_http) + @log_level = options.delete(:log_level) || ::Logger::WARN + @custom_logger = options.delete(:logger) + @custom_host = options.delete(:rest_host) + @custom_port = options.delete(:port) + @custom_tls_port = options.delete(:tls_port) + @add_request_ids = options.delete(:add_request_ids) + @log_retries_as_info = options.delete(:log_retries_as_info) + @max_message_size = options.delete(:max_message_size) || MAX_MESSAGE_SIZE + @max_frame_size = options.delete(:max_frame_size) || MAX_FRAME_SIZE + @idempotent_rest_publishing = options.delete_with_default(:idempotent_rest_publishing, true) + + if options[:fallback_hosts_use_default] && options[:fallback_hosts] + raise ArgumentError, "fallback_hosts_use_default cannot be set to try when fallback_hosts is also provided" + end + @fallback_hosts = case + when options.delete(:fallback_hosts_use_default) + Ably::FALLBACK_HOSTS + when options_fallback_hosts = options.delete(:fallback_hosts) + options_fallback_hosts + when custom_host || options[:realtime_host] || custom_port || custom_tls_port + [] + when environment + CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES.map { |host| "#{environment}#{host}" } + else + Ably::FALLBACK_HOSTS + end + + options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT + + # Take option keys prefixed with `http_`, remove the http_ and + # check if the option exists in HTTP_DEFAULTS. If so, update http_defaults + @http_defaults = HTTP_DEFAULTS.dup + options.each do |key, val| + if http_key = key[/^http_(.+)/, 1] + # Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout) + val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001) + @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym) + end + end + @http_defaults.freeze + + if @log_level == :none + @custom_logger = Ably::Models::NilLogger.new + else + @log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String) + end + + options.delete(:use_binary_protocol).tap do |use_binary_protocol| + if use_binary_protocol == true + @protocol = :msgpack + elsif use_binary_protocol == false + @protocol = :json + end + end + raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol) + + token_params = options.delete(:default_token_params) || {} + @options = options + init_auth_options = options.select do |key, _| + Auth::AUTH_OPTIONS_KEYS.include?(key.to_s) + end + + @auth = Auth.new(self, token_params, init_auth_options) + @channels = Ably::PubSub::Http::Channels.new(self) + @encoders = [] + + options.freeze + + initialize_default_encoders + end + + # Return a REST {Ably::PubSub::Http::Channel} for the given name + # + # @param (see Ably::PubSub::Http::Channels#get) + # + # @return (see Ably::PubSub::Http::Channels#get) + def channel(name, channel_options = {}) + channels.get(name, channel_options) + end + + # Queries the REST /stats API and retrieves your application's usage statistics. Returns a {Ably::Models::PaginatedResult} object, containing an array of {Ably::Models::Stats} objects. See the Stats docs. + # + # @spec RSC6a, RSC6b1, RSC6b2, RSC6b3, RSC6b4 + # + # @param [Hash] options the options for the stats request + # @option options [Integer,Time] :start The time from which stats are retrieved, specified as milliseconds since the Unix epoch. RSC6b1 + # @option options [Integer,Time] :end The time until stats are retrieved, specified as milliseconds since the Unix epoch. RSC6b1 + # @option options [Symbol] :direction The order for which stats are returned in. Valid values are backwards which orders stats from most recent to oldest, or forwards which orders stats from oldest to most recent. The default is backwards. RSC6b2 + # @option options [Integer] :limit An upper limit on the number of stats returned. The default is 100, and the maximum is 1000. RSC6b3 + # @option options [Symbol] :unit minute, hour, day or month. Based on the unit selected, the given start or end times are rounded down to the start of the relevant interval depending on the unit granularity of the query. RSC6b4 + # + # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::Stats} objects. + # + def stats(options = {}) + options = { + :direction => :backwards, + :unit => :minute, + :limit => 100 + }.merge(options) + + [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) } + raise ArgumentError, ":end must be equal to or after :start" if options[:start] && options[:end] && (options[:start] > options[:end]) + + paginated_options = { + coerce_into: 'Ably::Models::Stats' + } + + url = '/stats' + response = get(url, options) + + Ably::Models::PaginatedResult.new(response, url, self, paginated_options) + end + + # Retrieves the time from the Ably service as milliseconds since the Unix epoch. Clients that do not have access + # to a sufficiently well maintained time source and wish to issue Ably {Ably::Models::TokenRequest} with + # a more accurate timestamp should use the {Ably::PubSub::Http::Client#queryTime} property instead of this method. + # + # @spec RSC16 + # + # @return [Time] The time as milliseconds since the Unix epoch. + def time + response = get('/time', {}, send_auth_header: false) + + as_time_from_epoch(response.body.first) + end + + # @!attribute [r] use_tls? + # @return [Boolean] True if client is configured to use TLS for all Ably communication + def use_tls? + @tls == true + end + + # Perform an HTTP GET request to the API using configured authentication + # + # @return [Faraday::Response] + # + # @api private + def get(path, params = {}, options = {}) + raw_request(:get, path, params, options) + end + + # Perform an HTTP POST request to the API using configured authentication + # + # @return [Faraday::Response] + # + # @api private + def post(path, params, options = {}) + raw_request(:post, path, params, options) + end + + # Perform an HTTP PUT request to the API using configured authentication + # + # @return [Faraday::Response] + # + # @api private + def put(path, params, options = {}) + raw_request(:put, path, params, options) + end + + # Perform an HTTP DELETE request to the API using configured authentication + # + # @return [Faraday::Response] + # + # @api private + def delete(path, params, options = {}) + raw_request(:delete, path, params, options) + end + + # Makes a REST request to a provided path. This is provided as a convenience for developers who wish to use REST API + # functionality that is either not documented or is not yet included in the public API, without having to directly + # handle features such as authentication, paging, fallback hosts, MsgPack and JSON support. + # + # @spec RSC19 + # + # @param method [Symbol] The request method to use, such as :get, :post. + # @param path [String] The request path. + # @param params [Hash, nil] The parameters to include in the URL query of the request. The parameters depend on the endpoint being queried. See the REST API reference for the available parameters of each endpoint. + # @param body [Hash, nil] The JSON body of the request. + # @param headers [Hash, nil] Additional HTTP headers to include in the request. + # + # @return [Ably::Models::HttpPaginatedResponse<>] An {Ably::Models::HttpPaginatedResponse} object returned by the HTTP request, containing an empty or JSON-encodable object. + def request(method, path, params = {}, body = nil, headers = {}, options = {}) + raise "Method #{method.to_s.upcase} not supported" unless %i(get put patch post delete).include?(method.to_sym) + + response = case method.to_sym + when :get, :delete + reauthorize_on_authorization_failure do + send_request(method, path, params, headers: headers) + end + when :post, :patch, :put + if body.to_json.bytesize > max_frame_size + raise Ably::Exceptions::MaxFrameSizeExceeded.new("Maximum frame size exceeded #{max_frame_size} bytes.") + end + path_with_params = Addressable::URI.new + path_with_params.query_values = params || {} + query = path_with_params.query + reauthorize_on_authorization_failure do + send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers) + end + end + + paginated_options = { + async_blocking_operations: options.delete(:async_blocking_operations), + } + + Ably::Models::HttpPaginatedResponse.new(response, path, self, paginated_options) + + rescue Exceptions::ResourceMissing, Exceptions::ForbiddenRequest, Exceptions::ResourceMissing => e + response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message) + Models::HttpPaginatedResponse.new(response, path, self) + rescue Exceptions::TokenExpired, Exceptions::UnauthorizedRequest => e + response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message) + Models::HttpPaginatedResponse.new(response, path, self) + rescue Exceptions::InvalidRequest, Exceptions::ServerError => e + response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message) + Models::HttpPaginatedResponse.new(response, path, self) + end + + # Retrieves an object that represents the current state of the device as a target for push notifications. + # @spec RSH8 + # @return [Ably::Models::LocalDevice] + # + # @note This is unsupported in the Ruby library + def device + raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable' + end + + # A {Ably::PubSub::Http::Push} object. + # @spec RSH7 + # @return [Ably::PubSub::Http::Push] + def push + @push ||= Push.new(self) + end + + # @!attribute [r] endpoint + # @return [URI::Generic] Default Ably REST endpoint used for all requests + def endpoint + endpoint_for_host(custom_host || [@environment, DOMAIN].compact.join('-')) + end + + # @!attribute [r] logger + # @return [Logger] The {Ably::Logger} for this client. + # Configure the log_level with the `:log_level` option, refer to {Client#initialize} + def logger + @logger ||= Ably::Logger.new(self, log_level, @custom_logger) + end + + # @!attribute [r] mime_type + # @return [String] Mime type used for HTTP requests + def mime_type + case protocol + when :json + 'application/json' + else + 'application/x-msgpack' + end + end + + # Register a message encoder and decoder that implements Ably::Models::MessageEncoders::Base interface. + # Message encoders are used to encode and decode message payloads automatically. + # @note Encoders and decoders are processed in the order they are added so the first encoder will be given priority when encoding and decoding + # + # @param [Ably::Models::MessageEncoders::Base] encoder + # @return [void] + # + # @api private + def register_encoder(encoder, options = {}) + encoders << Ably::Models::MessageEncoders.encoder_from(encoder, options) + end + + # @!attribute [r] protocol_binary? + # @return [Boolean] True of the transport #protocol communicates with Ably with a binary protocol + def protocol_binary? + protocol == :msgpack + end + + # Connection used to make HTTP requests + # + # @param [Hash] options + # @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see the default {Ably::FALLBACK_HOSTS} + # + # @return [Faraday::Connection] + # + # @api private + def connection(options = {}) + if options[:use_fallback] + fallback_connection + else + @connection ||= Faraday.new(endpoint.to_s, connection_options) + end + end + + # Fallback connection used to make HTTP requests. + # Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback hosts} + # are used (unless custom fallback hosts are provided with fallback_hosts) + # + # @return [Faraday::Connection] + # + # @api private + def fallback_connection + unless defined?(@fallback_connections) && @fallback_connections + @fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) } + end + @fallback_index ||= 0 + + @fallback_connections[@fallback_index % @fallback_connections.count].tap do + @fallback_index += 1 + end + end + + # Allowable duration for an external auth request + # For REST client this defaults to request_timeout + # For Realtime clients this defaults to 250ms less than the realtime_request_timeout + # ensuring an auth failure will be triggered before the realtime request timeout fires + # which would lead to a misleading error message (connection timeout as opposed to auth request timeout) + # @api private + def auth_request_timeout + if @realtime_client + @realtime_client.connection.defaults.fetch(:realtime_request_timeout) - 0.25 + else + http_defaults.fetch(:request_timeout) + end + end + + # If the primary host endpoint fails, and a subsequent fallback host succeeds, the fallback + # host that succeeded is used for +ClientOption+ +fallback_retry_timeout+ seconds to avoid + # retries to known failing hosts for a short period of time. + # See https://github.com/ably/docs/pull/554, spec id #RSC15f + # + # @return [nil, String] Returns nil (falsey) if the primary host is being used, or the currently used host if a fallback host is currently preferred + def using_preferred_fallback_host? + if preferred_fallback_connection && (preferred_fallback_connection.fetch(:expires_at) > Time.now) + preferred_fallback_connection.fetch(:connection_object).host + end + end + + private + + attr_reader :preferred_fallback_connection + + # See #using_preferred_fallback_host? for context + def set_preferred_fallback_connection(connection) + @preferred_fallback_connection = if connection == @connection + # If the succeeded connection is in fact the primary connection (tried after a failed fallback) + # then clear the preferred fallback connection + nil + else + { + expires_at: Time.now + options.fetch(:fallback_retry_timeout), + connection_object: connection, + } + end + end + + def get_preferred_fallback_connection_object + preferred_fallback_connection.fetch(:connection_object) if using_preferred_fallback_host? + end + + def raw_request(method, path, params = {}, options = {}) + options = options.clone + if options.delete(:disable_automatic_reauthorize) == true + send_request(method, path, params, options) + else + reauthorize_on_authorization_failure do + send_request(method, path, params, options) + end + end + end + + # Sends HTTP request to connection end point + # Connection failures will automatically be reattempted until thresholds are met + def send_request(method, path, params, options) + max_retry_count = http_defaults.fetch(:max_retry_count) + max_retry_duration = http_defaults.fetch(:max_retry_duration) + requested_at = Time.now + retry_count = 0 + retry_sequence_id = nil + request_id = SecureRandom.urlsafe_base64(10) if add_request_ids + + preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object + + begin + use_fallback = can_fallback_to_alternate_ably_host? && (retry_count > 0) + + conn = if preferred_fallback_connection_for_first_request + case retry_count + when 0 + preferred_fallback_connection_for_first_request + when 1 + # Ensure the root host is used first if the preferred fallback fails, see #RSC15f + connection(use_fallback: false) + end + end || connection(use_fallback: use_fallback) # default to normal connection selection process if not preferred connection set + + conn.send(method, path, params) do |request| + if add_request_ids + request.params[:request_id] = request_id + request.options.context = {} if request.options.context.nil? + request.options.context[:request_id] = request_id + end + if options[:qs_params] + request.params.merge!(options[:qs_params]) + end + unless options[:send_auth_header] == false + request.headers[:authorization] = auth.auth_header + # RSA7e2 + options[:headers].to_h.merge(auth.extra_auth_headers).map do |key, val| + request.headers[key] = val + end + end + end.tap do + if retry_count > 0 + retry_log_severity = log_retries_as_info ? :info : :warn + logger.public_send(retry_log_severity) do + "Ably::PubSub::Http::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \ + " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)" + end + set_preferred_fallback_connection conn + end + end + + rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error + retry_sequence_id ||= SecureRandom.urlsafe_base64(4) + time_passed = Time.now - requested_at + + if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration) + retry_count += 1 + retry_log_severity = log_retries_as_info ? :info : :warn + logger.public_send(retry_log_severity) { "Ably::PubSub::Http::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" } + retry + end + + retry_log_severity = log_retries_as_info ? :info : :error + logger.public_send(retry_log_severity) do + "Ably::PubSub::Http::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \ + " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)" + end + + case error + when Faraday::TimeoutError + raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id }) + when *FARADAY_CLIENT_OR_SERVER_ERRORS + # request_id is also available in the request context + raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id }) + else + raise error + end + end + end + + def reauthorize_on_authorization_failure + yield + rescue Ably::Exceptions::TokenExpired => e + if auth.token_renewable? + auth.authorize + yield + else + raise e + end + end + + def endpoint_for_host(host) + port = if use_tls? + custom_tls_port + else + custom_port + end + + raise ArgumentError, "Custom port must be an Integer or nil" if port && !port.kind_of?(Integer) + + options = { + scheme: use_tls? ? 'https' : 'http', + host: host + } + options.merge!(port: port) if port + + URI::Generic.build(options) + end + + # Return a Hash of connection options to initiate the Faraday::Connection with + # + # @return [Hash] + def connection_options + @connection_options ||= { + builder: middleware, + headers: { + content_type: mime_type, + accept: mime_type, + user_agent: user_agent, + 'X-Ably-Version' => Ably::PROTOCOL_VERSION, + 'Ably-Agent' => agent + }, + request: { + open_timeout: http_defaults.fetch(:open_timeout), + timeout: http_defaults.fetch(:request_timeout) + } + } + end + + # Return a Faraday middleware stack to initiate the Faraday::RackBuilder with + # + # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ + def middleware + @middleware ||= Faraday::RackBuilder.new do |builder| + setup_outgoing_middleware builder + + # Raise exceptions if response code is invalid + builder.use Ably::PubSub::Http::Middleware::Exceptions + + setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true + + # Set Faraday's HTTP adapter with support for HTTP/2 + builder.adapter :typhoeus, http_version: :httpv2_0 + end + end + + def can_fallback_to_alternate_ably_host? + fallback_hosts && !fallback_hosts.empty? + end + + def initialize_default_encoders + Ably::Models::MessageEncoders.register_default_encoders self, binary_protocol: protocol == :msgpack + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/middleware/encoder.rb b/core/lib/ably/pubsub/http/middleware/encoder.rb new file mode 100644 index 000000000..cf512a6cb --- /dev/null +++ b/core/lib/ably/pubsub/http/middleware/encoder.rb @@ -0,0 +1,51 @@ +require 'faraday' +require 'json' + +module Ably + module PubSub + module Http + module Middleware + # Encode the body of the message according to the mime type + class Encoder < Faraday::Middleware + CONTENT_TYPE = 'Content-Type'.freeze unless defined? CONTENT_TYPE + + def call(env) + encode env if env.body + @app.call env + end + + private + def encode(env) + env.body = case request_type(env) + when 'application/x-msgpack' + to_msgpack(env.body) + when 'application/json', '', nil + env.request_headers[CONTENT_TYPE] = 'application/json' + to_json(env.body) + else + env.body + end + end + + def to_msgpack(body) + body.to_msgpack + end + + def to_json(body) + if body.kind_of?(String) + body + else + body.to_json + end + end + + def request_type(env) + type = env.request_headers[CONTENT_TYPE].to_s + type = type.split(';', 2).first if type.index(';') + type + end + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/middleware/exceptions.rb b/core/lib/ably/pubsub/http/middleware/exceptions.rb new file mode 100644 index 000000000..7fd2fb8bc --- /dev/null +++ b/core/lib/ably/pubsub/http/middleware/exceptions.rb @@ -0,0 +1,55 @@ +require 'faraday' +require 'json' + +module Ably + module PubSub + module Http + module Middleware + # HTTP exceptions raised by Ably due to an error status code + # Ably returns JSON/Msgpack error codes and messages so include this if possible in the exception messages + class Exceptions < Faraday::Middleware + def on_complete(env) + if env.status >= 400 + error_status_code = env.status + error_code = nil + + if env.body.kind_of?(Hash) + error = env.body.fetch('error', {}) + error_status_code = error['statusCode'].to_i if error['statusCode'] + error_code = error['code'].to_i if error['code'] + + if error + message = "#{error['message']} (status: #{error_status_code}, code: #{error_code})" + else + message = env.body + end + else + message = env.body + end + + message = 'Unknown server error' if message.to_s.strip == '' + request_id = env.request.context[:request_id] if env.request.context + exception_args = [message, error_status_code, error_code, nil, { request_id: request_id }] + + if env.status >= 500 + raise Ably::Exceptions::ServerError.new(*exception_args) + elsif env.status == 401 + if Ably::Exceptions::TOKEN_EXPIRED_CODE.include?(error_code) + raise Ably::Exceptions::TokenExpired.new(*exception_args) + else + raise Ably::Exceptions::UnauthorizedRequest.new(*exception_args) + end + elsif env.status == 403 + raise Ably::Exceptions::ForbiddenRequest.new(*exception_args) + elsif env.status == 404 + raise Ably::Exceptions::ResourceMissing.new(*exception_args) + else + raise Ably::Exceptions::InvalidRequest.new(*exception_args) + end + end + end + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/middleware/external_exceptions.rb b/core/lib/ably/pubsub/http/middleware/external_exceptions.rb new file mode 100644 index 000000000..c9c52d7ab --- /dev/null +++ b/core/lib/ably/pubsub/http/middleware/external_exceptions.rb @@ -0,0 +1,26 @@ +require 'faraday' + +module Ably + module PubSub + module Http + module Middleware + # HTTP exceptions raised due to a status code error on a 3rd party site + # Used by auth calls + class ExternalExceptions < Faraday::Middleware + def on_complete(env) + if env.status >= 400 + error_status_code = env.status + message = "Error #{error_status_code}: #{(env.body || '')[0...200]}" + + if error_status_code >= 500 + raise Ably::Exceptions::ServerError, message + else + raise Ably::Exceptions::InvalidRequest, message + end + end + end + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/middleware/fail_if_unsupported_mime_type.rb b/core/lib/ably/pubsub/http/middleware/fail_if_unsupported_mime_type.rb new file mode 100644 index 000000000..23fe0faed --- /dev/null +++ b/core/lib/ably/pubsub/http/middleware/fail_if_unsupported_mime_type.rb @@ -0,0 +1,24 @@ +require 'faraday' +require 'json' + +module Ably + module PubSub + module Http + module Middleware + class FailIfUnsupportedMimeType < Faraday::Middleware + def on_complete(env) + unless env.response_headers['Ably-Middleware-Parsed'] == true + # Ignore empty body with success status code for no body response + return if env.body.to_s.empty? && env.status == 204 + + unless (500..599).include?(env.status) + raise Ably::Exceptions::InvalidResponseBody, + "Content Type #{env.response_headers['Content-Type']} is not supported by this client library" + end + end + end + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/middleware/logger.rb b/core/lib/ably/pubsub/http/middleware/logger.rb new file mode 100644 index 000000000..aeb7da5a9 --- /dev/null +++ b/core/lib/ably/pubsub/http/middleware/logger.rb @@ -0,0 +1,60 @@ +require 'faraday' + +module Ably + module PubSub + module Http + module Middleware + class Logger < Faraday::Middleware + extend Forwardable + + def initialize(app, logger = nil) + super(app) + @logger = logger || begin + require 'logger' + ::Logger.new(STDOUT) + end + end + + def_delegators :@logger, :debug, :info, :warn, :error, :fatal + + def call(env) + debug { "=> URL: #{env.method} #{env.url}, Headers: #{dump_headers env.request_headers}" } + debug { "=> Body: #{body_for(env)}" } + super + end + + def on_complete(env) + debug "<= Status: #{env.status}, Headers: #{dump_headers env.response_headers}" + debug "<= Body: #{body_for(env)}" + end + + private + def dump_headers(headers) + headers.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") + end + + def body_for(env) + return '' if !env.body || env.body.empty? + + if env.request_headers['Content-Type'] == 'application/x-msgpack' + MessagePack.unpack(env.body) + else + env.body + end + + rescue StandardError + readable_body(env.body) + end + + def readable_body(body) + if body.respond_to?(:encoding) && body.encoding == Encoding::ASCII_8BIT + body.unpack('H*') + else + body + end + end + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/middleware/parse_json.rb b/core/lib/ably/pubsub/http/middleware/parse_json.rb new file mode 100644 index 000000000..fb66b753e --- /dev/null +++ b/core/lib/ably/pubsub/http/middleware/parse_json.rb @@ -0,0 +1,29 @@ +require 'faraday' +require 'json' + +module Ably + module PubSub + module Http + module Middleware + class ParseJson < Faraday::Middleware + def on_complete(env) + if env.response_headers['Content-Type'] == 'application/json' + env.body = parse(env.body) unless env.response_headers['Ably-Middleware-Parsed'] == true + env.response_headers['Ably-Middleware-Parsed'] = true + end + end + + def parse(body) + if body.length > 0 + JSON.parse(body) + else + body + end + rescue JSON::ParserError => e + raise Ably::Exceptions::InvalidResponseBody, "Expected JSON response: #{e.message}" + end + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/middleware/parse_message_pack.rb b/core/lib/ably/pubsub/http/middleware/parse_message_pack.rb new file mode 100644 index 000000000..37873eaec --- /dev/null +++ b/core/lib/ably/pubsub/http/middleware/parse_message_pack.rb @@ -0,0 +1,45 @@ +require 'faraday' +require 'msgpack' + +module Ably + module PubSub + module Http + module Middleware + class ParseMessagePack < Faraday::Middleware + def on_complete(env) + if env.response_headers['Content-Type'] == 'application/x-msgpack' + env.body = parse(env.body) unless env.response_headers['Ably-Middleware-Parsed'] == true + env.response_headers['Ably-Middleware-Parsed'] = true + end + rescue Ably::Exceptions::InvalidResponseBody => e + debug_info = { + method: env.method, + url: env.url, + base64_body: base64_body(env.body), + response_headers: env.response_headers + } + raise Ably::Exceptions::InvalidResponseBody, "#{e.message}\nRequest env: #{debug_info}" + end + + def parse(body) + if body.length > 0 + MessagePack.unpack(body) + else + body + end + rescue MessagePack::UnknownExtTypeError => e + raise Ably::Exceptions::InvalidResponseBody, "MessagePack::UnknownExtTypeError body could not be decoded: #{e.message}. Got Base64:\n#{base64_body(body)}" + rescue MessagePack::MalformedFormatError => e + raise Ably::Exceptions::InvalidResponseBody, "MessagePack::MalformedFormatError body could not be decoded: #{e.message}. Got Base64:\n#{base64_body(body)}" + end + + def base64_body(body) + Base64.encode64(body) + rescue => err + "[#{err.message}! Could not base64 encode body: '#{body}']" + end + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/presence.rb b/core/lib/ably/pubsub/http/presence.rb new file mode 100644 index 000000000..b2d11e38a --- /dev/null +++ b/core/lib/ably/pubsub/http/presence.rb @@ -0,0 +1,117 @@ +module Ably + module PubSub + module Http + # Enables the retrieval of the current and historic presence set for a channel. + # + class Presence + include Ably::Modules::Conversions + + # {Ably::PubSub::Http::Client} for this Presence object + # + # @return {Ably::PubSub::Http::Client} + # + # @private + attr_reader :client + + # {Ably::PubSub::Http::Channel} this Presence object is associated with + # + # @return [Ably::PubSub::Http::Channel] + # + attr_reader :channel + + # Initialize a new Presence object + # + # @param client [Ably::PubSub::Http::Client] + # @param channel [Channel] The channel object + # + def initialize(client, channel) + @client = client + @channel = channel + end + + # Retrieves the current members present on the channel and the metadata for each member, such as their + # {Ably::Models::PresenceMessage::ACTION} and ID. Returns a {Ably::Models::PaginatedResult} object, + # containing an array of {Ably::Models::PresenceMessage} objects. + # + # @spec RSPa, RSP3a, RSP3a2, RSP3a3 + # + # @param [Hash] options the options for the set of members present + # @option options [Integer] :limit An upper limit on the number of messages returned. The default is 100, and the maximum is 1000. (RSP3a) + # @option options [String] :client_id Filters the list of returned presence members by a specific client using its ID. (RSP3a2) + # @option options [String] :connection_id Filters the list of returned presence members by a specific connection using its ID. (RSP3a3) + # + # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::PresenceMessage} objects. + # + def get(options = {}) + options = options = { + :limit => 100 + }.merge(options) + + paginated_options = { + coerce_into: 'Ably::Models::PresenceMessage', + async_blocking_operations: options.delete(:async_blocking_operations), + } + + # The REST API names these query params clientId and connectionId (RSP3a2, RSP3a3), + # so the idiomatic snake_case options have to be converted before they are sent. + response = client.get(base_path, IdiomaticRubyWrapper(options).as_json) + + Ably::Models::PaginatedResult.new(response, base_path, client, paginated_options) do |presence_message| + presence_message.tap do |message| + decode_message message + end + end + end + + # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical {Ably::Models::PresenceMessage} + # objects for the channel. If the channel is configured to persist messages, then presence messages can be retrieved + # from history for up to 72 hours in the past. If not, presence messages can only be retrieved from history for up to two minutes in the past. + # + # @spec RSP4a + # + # @param [Hash] options the options for the message history request + # @option options [Integer,Time] :start The time from which messages are retrieved, specified as milliseconds since the Unix epoch. (RSP4b1) + # @option options [Integer,Time] :end The time until messages are retrieved, specified as milliseconds since the Unix epoch. (RSP4b1) + # @option options [Symbol] :direction The order for which messages are returned in. Valid values are backwards which orders messages from most recent to oldest, or forwards which orders messages from oldest to most recent. The default is backwards. (RSP4b2) + # @option options [Integer] :limit An upper limit on the number of messages returned. The default is 100, and the maximum is 1000. (RSP4b3) + # + # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::PresenceMessage} objects. + # + def history(options = {}) + url = "#{base_path}/history" + options = options = { + :direction => :backwards, + :limit => 100 + }.merge(options) + + [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) } + raise ArgumentError, ":end must be equal to or after :start" if options[:start] && options[:end] && (options[:start] > options[:end]) + + paginated_options = { + coerce_into: 'Ably::Models::PresenceMessage', + async_blocking_operations: options.delete(:async_blocking_operations), + } + + response = client.get(url, options) + + Ably::Models::PaginatedResult.new(response, url, client, paginated_options) do |presence_message| + presence_message.tap do |message| + decode_message message + end + end + end + + private + def base_path + "/channels/#{URI.encode_www_form_component(channel.name)}/presence" + end + + def decode_message(presence_message) + presence_message.decode client.encoders, channel.options + rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e + client.logger.error { "Decoding Error on presence channel '#{channel.name}', presence message client_id '#{presence_message.client_id}'. #{e.class.name}: #{e.message}" } + end + end + end + end +end diff --git a/core/lib/ably/pubsub/http/push.rb b/core/lib/ably/pubsub/http/push.rb new file mode 100644 index 000000000..28313df22 --- /dev/null +++ b/core/lib/ably/pubsub/http/push.rb @@ -0,0 +1,27 @@ +require 'ably/pubsub/http/push/admin' + +module Ably + module PubSub + module Http + # Class providing push notification functionality + class Push + include Ably::Modules::Conversions + + # @private + attr_reader :client + + def initialize(client) + @client = client + end + + # Admin features for push notifications like managing devices and channel subscriptions + # + # @return [Ably::PubSub::Http::Push::Admin] + # + def admin + @admin ||= Admin.new(self) + end + end + end + end +end diff --git a/core/lib/ably/rest/push/admin.rb b/core/lib/ably/pubsub/http/push/admin.rb similarity index 88% rename from core/lib/ably/rest/push/admin.rb rename to core/lib/ably/pubsub/http/push/admin.rb index fcc177ea7..372c0831a 100644 --- a/core/lib/ably/rest/push/admin.rb +++ b/core/lib/ably/pubsub/http/push/admin.rb @@ -1,7 +1,7 @@ -require 'ably/rest/push/device_registrations' -require 'ably/rest/push/channel_subscriptions' +require 'ably/pubsub/http/push/device_registrations' +require 'ably/pubsub/http/push/channel_subscriptions' -module Ably::Rest +module Ably::PubSub::Http class Push # Class providing push notification administrative functionality # for registering devices and attaching to channels etc. @@ -40,7 +40,7 @@ def publish(recipient, data) # Manage device registrations # - # @return [Ably::Rest::Push::DeviceRegistrations] + # @return [Ably::PubSub::Http::Push::DeviceRegistrations] # def device_registrations @device_registrations ||= DeviceRegistrations.new(self) @@ -48,7 +48,7 @@ def device_registrations # Manage channel subscriptions for devices or clients # - # @return [Ably::Rest::Push::ChannelSubscriptions] + # @return [Ably::PubSub::Http::Push::ChannelSubscriptions] # def channel_subscriptions @channel_subscriptions ||= ChannelSubscriptions.new(self) diff --git a/core/lib/ably/rest/push/channel_subscriptions.rb b/core/lib/ably/pubsub/http/push/channel_subscriptions.rb similarity index 99% rename from core/lib/ably/rest/push/channel_subscriptions.rb rename to core/lib/ably/pubsub/http/push/channel_subscriptions.rb index 7bdebd438..a5aca8973 100644 --- a/core/lib/ably/rest/push/channel_subscriptions.rb +++ b/core/lib/ably/pubsub/http/push/channel_subscriptions.rb @@ -1,4 +1,4 @@ -module Ably::Rest +module Ably::PubSub::Http class Push # Manage push notification channel subscriptions for devices or client identifiers class ChannelSubscriptions diff --git a/core/lib/ably/rest/push/device_registrations.rb b/core/lib/ably/pubsub/http/push/device_registrations.rb similarity index 99% rename from core/lib/ably/rest/push/device_registrations.rb rename to core/lib/ably/pubsub/http/push/device_registrations.rb index f5eb22874..a083886ef 100644 --- a/core/lib/ably/rest/push/device_registrations.rb +++ b/core/lib/ably/pubsub/http/push/device_registrations.rb @@ -1,4 +1,4 @@ -module Ably::Rest +module Ably::PubSub::Http class Push # Manage device registrations for push notifications class DeviceRegistrations diff --git a/core/lib/ably/pubsub/realtime.rb b/core/lib/ably/pubsub/realtime.rb new file mode 100644 index 000000000..4f17787bc --- /dev/null +++ b/core/lib/ably/pubsub/realtime.rb @@ -0,0 +1,58 @@ +require 'eventmachine' +require 'websocket/driver' +require 'em-http-request' + +require 'ably/modules/event_emitter' + +require 'ably/pubsub/realtime/auth' +require 'ably/pubsub/realtime/channel' +require 'ably/pubsub/realtime/channels' +require 'ably/pubsub/realtime/client' +require 'ably/pubsub/realtime/connection' +require 'ably/pubsub/realtime/push' +require 'ably/pubsub/realtime/presence' + +require 'ably/models/message_encoders/base' + +Dir.glob(File.expand_path("../models/*.rb", File.dirname(__FILE__))).each do |file| + require file +end + +Dir.glob(File.expand_path("realtime/models/*.rb", File.dirname(__FILE__))).each do |file| + require file +end + +require 'ably/models/message_encoders/base' + +require 'ably/pubsub/realtime/client/incoming_message_dispatcher' +require 'ably/pubsub/realtime/client/outgoing_message_dispatcher' + +module Ably + module PubSub + # Realtime is the namespace of the stateful realtime client and the models it returns. + # + # @example + # client = Ably::PubSub::Server.create_realtime_client("xxxxx") + # channel = client.channel("test") + # channel.subscribe do |message| + # message[:name] #=> "greeting" + # end + # channel.publish "greeting", "data" + # + module Realtime + # Refuses construction. This was a convenience alias for the {Ably::PubSub::Realtime::Client} + # constructor, which no longer accepts direct construction: the package a client is + # created from is what declares the client's side to the platform, and a client + # constructed here declares none. + # + # Use {Ably::PubSub::Server.create_realtime_client} from the +ably-pubsub-server+ gem. + # + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::PubSub::Realtime.new', 'Ably::PubSub::Server.create_realtime_client(options)' + ) + end + end + end +end diff --git a/core/lib/ably/pubsub/realtime/auth.rb b/core/lib/ably/pubsub/realtime/auth.rb new file mode 100644 index 000000000..555c0732f --- /dev/null +++ b/core/lib/ably/pubsub/realtime/auth.rb @@ -0,0 +1,285 @@ +require 'ably/auth' + +module Ably + module PubSub + module Realtime + # Auth is responsible for authentication with {https://www.ably.com Ably} using basic or token authentication + # This {Ably::PubSub::Realtime::Auth Realtime::Auth} class wraps the {Ably::Auth Synchronous Ably::Auth} class in an EventMachine friendly way using Deferrables for all IO. See {Ably::Auth Ably::Auth} for more information + # + # Find out more about Ably authentication at: https://www.ably.com/docs/general/authentication/ + # + # @!attribute [r] client_id + # (see Ably::Auth#client_id) + # @!attribute [r] current_token_details + # (see Ably::Auth#current_token_details) + # @!attribute [r] token + # (see Ably::Auth#token) + # @!attribute [r] key + # (see Ably::Auth#key) + # @!attribute [r] key_name + # (see Ably::Auth#key_name) + # @!attribute [r] key_secret + # (see Ably::Auth#key_secret) + # @!attribute [r] options + # (see Ably::Auth#options) + # @!attribute [r] token_params + # (see Ably::Auth#options) + # @!attribute [r] using_basic_auth? + # (see Ably::Auth#using_basic_auth?) + # @!attribute [r] using_token_auth? + # (see Ably::Auth#using_token_auth?) + # @!attribute [r] token_renewable? + # (see Ably::Auth#token_renewable?) + # @!attribute [r] authentication_security_requirements_met? + # (see Ably::Auth#authentication_security_requirements_met?) + # + class Auth + extend Forwardable + include Ably::Modules::AsyncWrapper + + def_delegators :auth_sync, :client_id + def_delegators :auth_sync, :token_client_id_allowed?, :configure_client_id, :client_id_validated? + def_delegators :auth_sync, :can_assume_client_id?, :has_client_id? + def_delegators :auth_sync, :current_token_details, :token + def_delegators :auth_sync, :key, :key_name, :key_secret, :options, :auth_options, :token_params + def_delegators :auth_sync, :using_basic_auth?, :using_token_auth? + def_delegators :auth_sync, :token_renewable?, :authentication_security_requirements_met? + def_delegators :client, :logger + def_delegators :client, :connection + + def initialize(client) + @client = client + @auth_sync = client.rest_client.auth + end + + # For new connections, ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. + # If a connection is already established, the connection will be upgraded with a new token + # + # In the event that a new token request is made, the provided options are used + # + # @param (see Ably::Auth#authorize) + # @option (see Ably::Auth#authorize) + # + # @return [Ably::Util::SafeDeferrable] + # @yield [Ably::Models::TokenDetails] + # + # @example + # # will issue a simple token request using basic auth + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') + # client.auth.authorize do |token_details| + # token_details #=> Ably::Models::TokenDetails + # end + # + def authorize(token_params = nil, auth_options = nil, &success_callback) + Ably::Util::SafeDeferrable.new(logger).tap do |authorize_method_deferrable| + # Wrap the sync authorize method and wait for the result from the deferrable + async_wrap do + authorize_sync(token_params, auth_options) + end.tap do |auth_operation| + # Authorize operation succeeded and we have a new token, now let's perform inline authentication + auth_operation.callback do |token| + case connection.state.to_sym + when :initialized, :disconnected, :suspended, :closed, :closing, :failed + connection.connect + when :connected + perform_inline_auth token + when :connecting + # Fail all current connection attempts and try again with the new token, see #RTC8b + connection.manager.release_and_establish_new_transport + else + logger.fatal { "Auth#authorize: unsupported state #{connection.state}" } + authorize_method_deferrable.fail Ably::Exceptions::InvalidState.new("Unsupported state #{connection.state} for Auth#authorize") + next + end + + # Indicate success or failure based on response from realtime, see #RTC8b1 + auth_deferrable_resolved = false + + connection.unsafe_once(:connected, :update) do + auth_deferrable_resolved = true + authorize_method_deferrable.succeed token + end + connection.unsafe_once(:suspended, :closed, :failed) do |state_change| + auth_deferrable_resolved = true + authorize_method_deferrable.fail state_change.reason + end + end + + # Authorize failed, likely due to auth_url or auth_callback failing + auth_operation.errback do |error| + client.connection.transition_state_machine :failed, reason: error if error.kind_of?(Ably::Exceptions::IncompatibleClientId) + authorize_method_deferrable.fail error + end + end + + # Call the block provided to this method upon success of this deferrable + authorize_method_deferrable.callback do |token| + yield token if block_given? + end + end + end + + # @deprecated Use {#authorize} instead + def authorise(*args, &block) + logger.warn { "Auth#authorise is deprecated and will be removed in 1.0. Please use Auth#authorize instead" } + authorize(*args, &block) + end + + # Synchronous version of {#authorize}. See {Ably::Auth#authorize} for method definition + # Please note that authorize_sync will however not upgrade the current connection's token as this requires + # an synchronous operation to send the new authentication details to Ably over a realtime connection + # + # @param (see Ably::Auth#authorize) + # @option (see Ably::Auth#authorize) + # @return [Ably::Models::TokenDetails] + # + def authorize_sync(token_params = nil, auth_options = nil) + @authorization_in_flight = true + auth_sync.authorize(token_params, auth_options) + ensure + @authorization_in_flight = false + end + + # @api private + def authorization_in_flight? + @authorization_in_flight + end + + # @deprecated Use {#authorize_sync} instead + def authorise_sync(*args) + logger.warn { "Auth#authorise_sync is deprecated and will be removed in 1.0. Please use Auth#authorize_sync instead" } + authorize_sync(*args) + end + + # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests + # + # @param (see Ably::Auth#request_token) + # @option (see Ably::Auth#request_token) + # + # @return [Ably::Util::SafeDeferrable] + # @yield [Ably::Models::TokenDetails] + # + # @example + # # simple token request using basic auth + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') + # client.auth.request_token do |token_details| + # token_details #=> Ably::Models::TokenDetails + # end + # + def request_token(token_params = {}, auth_options = {}, &success_callback) + async_wrap(success_callback) do + request_token_sync(token_params, auth_options) + end + end + + # Synchronous version of {#request_token}. See {Ably::Auth#request_token} for method definition + # @param (see Ably::Auth#authorize) + # @option (see Ably::Auth#authorize) + # @return [Ably::Models::TokenDetails] + # + def request_token_sync(token_params = {}, auth_options = {}) + auth_sync.request_token(token_params, auth_options) + end + + # Creates and signs a token request that can then subsequently be used by any client to request a token + # + # @param (see Ably::Auth#create_token_request) + # @option (see Ably::Auth#create_token_request) + # + # @return [Ably::Util::SafeDeferrable] + # @yield [Models::TokenRequest] + # + # @example + # client.auth.create_token_request({ ttl: 3600 }, id: 'asd.asd') do |token_request| + # token_request #=> Ably::Models::TokenRequest + # end + def create_token_request(token_params = {}, auth_options = {}, &success_callback) + async_wrap(success_callback) do + create_token_request_sync(token_params, auth_options) + end + end + + # Synchronous version of {#create_token_request}. See {Ably::Auth#create_token_request} for method definition + # @param (see Ably::Auth#authorize) + # @option (see Ably::Auth#authorize) + # @return [Ably::Models::TokenRequest] + # + def create_token_request_sync(token_params = {}, auth_options = {}) + auth_sync.create_token_request(token_params, auth_options) + end + + # Auth header string used in HTTP requests to Ably + # Will reauthorize implicitly if required and capable + # + # @return [Ably::Util::SafeDeferrable] + # @yield [String] HTTP authentication value used in HTTP_AUTHORIZATION header + # + def auth_header(&success_callback) + async_wrap(success_callback) do + auth_header_sync + end + end + + # Synchronous version of {#auth_header}. See {Ably::Auth#auth_header} for method definition + # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header + # + def auth_header_sync + auth_sync.auth_header + end + + def client_id_for_request_sync + auth_sync.client_id_for_request + end + + # Auth params used in URI endpoint for Realtime connections + # Will reauthorize implicitly if required and capable + # + # @return [Ably::Util::SafeDeferrable] + # @yield [Hash] Auth params for a new Realtime connection + # + def auth_params(&success_callback) + fail_callback = lambda do |error, deferrable| + logger.error { "Failed to authenticate: #{error}" } + if error.kind_of?(Ably::Exceptions::BaseAblyException) + # Use base exception if it exists carrying forward the status codes + deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, nil, nil, error) + else + deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, 500, Ably::Exceptions::Codes::CLIENT_CONFIGURED_AUTHENTICATION_PROVIDER_REQUEST_FAILED) + end + end + async_wrap(success_callback, fail_callback) do + auth_params_sync + end + end + + # Synchronous version of {#auth_params}. See {Ably::Auth#auth_params} for method definition + # @return [Hash] Auth params for a new Realtime connection + # + def auth_params_sync + auth_sync.auth_params + end + + private + # The synchronous Auth class instanced by the HTTP client + # @return [Ably::Auth] + def auth_sync + @auth_sync + end + + def client + @client + end + + # Sends an AUTH ProtocolMessage on the existing connection triggering + # an inline AUTH process, see #RTC8a + def perform_inline_auth(token) + logger.debug { "Performing inline AUTH with Ably using token #{token}" } + connection.send_protocol_message( + action: Ably::Models::ProtocolMessage::ACTION.Auth.to_i, + auth: { access_token: token.token } + ) + end + end + end + end +end diff --git a/core/lib/ably/pubsub/realtime/channel.rb b/core/lib/ably/pubsub/realtime/channel.rb new file mode 100644 index 000000000..a5de314f0 --- /dev/null +++ b/core/lib/ably/pubsub/realtime/channel.rb @@ -0,0 +1,442 @@ +require 'ably/pubsub/realtime/channel/publisher' + +module Ably + module PubSub + module Realtime + # Enables messages to be published and subscribed to. Also enables historic messages to be retrieved and provides + # access to the {Ably::PubSub::Realtime::Channel} object of a channel. + # + # Channels will always be in one of the following states: + # + # initialized: 0 + # attaching: 1 + # attached: 2 + # detaching: 3 + # detached: 4 + # failed: 5 + # + # Note that the states are available as Enum-like constants: + # + # Channel::STATE.Initialized The channel has been initialized but no attach has yet been attempted. + # Channel::STATE.Attaching An attach has been initiated by sending a request to Ably. + # This is a transient state, followed either by a transition to ATTACHED, SUSPENDED, or FAILED. + # Channel::STATE.Attached The attach has succeeded. In the ATTACHED state a client may publish and subscribe to messages, or be present on the channel. + # Channel::STATE.Detaching A detach has been initiated on an ATTACHED channel by sending a request to Ably. + # This is a transient state, followed either by a transition to DETACHED or FAILED. + # Channel::STATE.Detached The channel, having previously been ATTACHED, has been detached by the user. + # Channel::STATE.Suspended The channel, having previously been ATTACHED, has lost continuity, usually due to + # the client being disconnected from Ably for longer than two minutes. It will automatically attempt to reattach as soon as connectivity is restored. + # Channel::STATE.Failed An indefinite failure condition. This state is entered if a channel error + # has been received from the Ably service, such as an attempt to attach without the necessary access rights. + # + class Channel + include Ably::Modules::Conversions + include Ably::Modules::EventEmitter + include Ably::Modules::EventMachineHelpers + include Ably::Modules::AsyncWrapper + include Ably::Modules::MessageEmitter + include Ably::PubSub::Realtime::Channel::Publisher + extend Ably::Modules::Enum + extend Forwardable + + # The current {Abbly::Realtime::Channel::STATE} of the channel. + # + # @spec RTL2b + # + # The permitted states for this channel + STATE = ruby_enum('STATE', + :initialized, + :attaching, + :attached, + :detaching, + :detached, + :suspended, + :failed + ) + + # Describes the events emitted by a {Ably::PubSub::Http::Channel} or {Ably::PubSub::Realtime::Channel} object. + # An event is either an UPDATE or a {Ably::PubSub::Http::Channel::STATE}. + # + # The permitted channel events that are emitted for this channel + # + # @spec RTL2g + # + EVENT = ruby_enum('EVENT', + STATE.to_sym_arr + [:update] + ) + + include Ably::Modules::StateEmitter + include Ably::Modules::UsesStateMachine + ensure_state_machine_emits 'Ably::Models::ChannelStateChange' + + # Max number of messages to bundle in a single ProtocolMessage + MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50 + + # {Ably::PubSub::Realtime::Client} associated with this channel + # + # @return [Ably::PubSub::Realtime::Client] + # + # @api private + attr_reader :client + + # The channel name. + # @return [String] + attr_reader :name + + # A {Ably::PubSub::Realtime::Channel::PushChannel} object. + # + # @return [Ably::PubSub::Realtime::Channel::PushChannel] + attr_reader :push + + # Channel options configured for this channel, see {#initialize} for channel_options + # @return [Hash] + attr_reader :options + + # A {Ably::PubSub::Realtime::Channel::ChannelProperties} object. + # + # @spec CP1, RTL15 + # + # @return [{Ably::PubSub::Realtime::Channel::ChannelProperties}] + attr_reader :properties + + # An {Ably::Models::ErrorInfo} object describing the last error which occurred on the channel, if any. + # @spec RTL4e + # @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException] + attr_reader :error_reason + + # The Channel manager responsible for attaching, detaching and handling failures for this channel + # @return [Ably::PubSub::Realtime::Channel::ChannelManager] + # @api private + attr_reader :manager + + # Flag that specifies whether channel is resuming attachment(reattach) or is doing a 'clean attach' RTL4j1 + # @return [Boolean] + # @api private + attr_reader :attach_resume + + # Optional channel parameters that configure the behavior of the channel. + # @spec RTL4k1 + # return [Hash] + def_delegators :options, :params + + # Initialize a new Channel object + # + # @param client [Ably::PubSub::Http::Client] + # @param name [String] The name of the channel + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} + # + def initialize(client, name, channel_options = {}) + name = ensure_utf_8(:name, name) + + @options = Ably::Models::ChannelOptions(channel_options) + @client = client + @name = name + @queue = [] + + @state_machine = ChannelStateMachine.new(self) + @state = STATE(state_machine.current_state) + @manager = ChannelManager.new(self, client.connection) + @push = PushChannel.new(self) + @properties = ChannelProperties.new(self) + @attach_resume = false + + setup_event_handlers + setup_presence + end + + # Publish a message to the channel. A callback may optionally be passed in to this call to be notified of success + # or failure of the operation. When publish is called with this client library, it won't attempt to implicitly + # attach to the channel. + # + # @spec RTL6i + # + # @param name [String, Array, 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 + # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument + # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string + # + # @yield [Ably::Models::Message,Array] On success, will call the block with the {Ably::Models::Message} if a single message is published, or an Array of {Ably::Models::Message} when multiple messages are published + # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks + # + # @example + # # Publish a single message form + # channel.publish 'click', { x: 1, y: 2 } + # + # # Publish a single message with single Hash form + # message = { name: 'click', data: { x: 1, y: 2 } } + # channel.publish message + # + # # Publish an array of message Hashes form + # messages = [ + # { name: 'click', data: { x: 1, y: 2 } }, + # { name: 'click', data: { x: 2, y: 3 } } + # ] + # channel.publish messages + # + # # Publish an array of Ably::Models::Message objects form + # messages = [ + # Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) + # Ably::Models::Message(name: 'click', data: { x: 2, y: 3 }) + # ] + # channel.publish messages + # + # # Publish an array of Ably::Models::Message objects form + # message = Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) + # channel.publish message + # + # channel.publish('click', 'body') do |message| + # puts "#{message.name} event received with #{message.data}" + # end + # + # channel.publish('click', 'body').errback do |error, message| + # puts "#{message.name} was not received, error #{error.message}" + # end + # + def publish(name, data = nil, attributes = {}, &success_block) + if suspended? || failed? + error = Ably::Exceptions::ChannelInactive.new("Cannot publish messages on a channel in state #{state}") + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + if !connection.can_publish_messages? + error = Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. 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 + + messages = build_messages(name, data, attributes) # (RSL1a, RSL1b) + + if messages.length > Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE + error = Ably::Exceptions::InvalidRequest.new("It is not possible to publish more than #{Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE} messages with a single publish request.") + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + enqueue_messages_on_connection(client, messages, channel_name, options).tap do |deferrable| + deferrable.callback(&success_block) if block_given? + end + end + + # Registers a listener for messages on this channel. The caller supplies a listener function, which is called + # each time one or more messages arrives on the channel. A callback may optionally be passed in to this call + # to be notified of success or failure of the channel {Ably::PubSub::Realtime::Channel#attach} operation. + # + # @spec RTL7a + # + # @param names [String] The event name of the message to subscribe to if provided. Defaults to all events. + # @yield [Ably::Models::Message] For each message received, the block is called + # + # @return [void] + # + def subscribe(*names, &callback) + attach unless attached? || attaching? + super + end + + # Deregisters the given listener for the specified event name(s). This removes an earlier event-specific subscription. + # + # @spec RTL8a + # + # @param names [String] The event name of the message to subscribe to if provided. Defaults to all events. + # + # @return [void] + # + def unsubscribe(*names, &callback) + super + end + + # Attach to this channel ensuring the channel is created in the Ably system and all messages published on + # the channel are received by any channel listeners registered using {Ably::PubSub::Realtime::Channel#subscribe}. + # Any resulting channel state change will be emitted to any listeners registered using the {Ably::Modules::EventEmitter#on} + # or {Ably::Modules::EventEmitter#once} methods. A callback may optionally be passed in to this call to be notified + # of success or failure of the operation. As a convenience, attach() is called implicitly + # if {Ably::PubSub::Realtime::Channel#subscribe} for the channel is called, or {Ably::PubSub::Realtime::Presence#enter} + # or {Ably::PubSub::Realtime::Presence#subscribe} are called on the {Ably::PubSub::Realtime::Presence} object for this channel. + # + # @spec RTL4d + # + # @yield [Ably::PubSub::Realtime::Channel] Block is called as soon as this channel is in the Attached state + # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback + # + def attach(&success_block) + if connection.closing? || connection.closed? || connection.suspended? || connection.failed? + error = Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}") + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + if !attached? + if detaching? + # Let the pending operation complete (#RTL4h) + once_state_changed { transition_state_machine :attaching if can_transition_to?(:attaching) } + else + transition_state_machine :attaching if can_transition_to?(:attaching) + end + end + + deferrable_for_state_change_to(STATE.Attached, &success_block) + end + + # Detach from this channel. Any resulting channel state change is emitted to any listeners registered using + # the {Ably::Modules::EventEmitter#on} or {Ably::Modules::EventEmitter#once} methods. A callback may optionally + # be passed in to this call to be notified of success or failure of the operation. Once all clients globally + # have detached from the channel, the channel will be released in the Ably service within two minutes. + # + # @spec RTL5e + # + # @yield [Ably::PubSub::Realtime::Channel] Block is called as soon as this channel is in the Detached or Failed state + # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback + # + def detach(&success_block) + if initialized? + success_block.call if block_given? + return Ably::Util::SafeDeferrable.new_and_succeed_immediately(logger) + end + + if failed? || connection.closing? || connection.failed? + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:detaching)) + end + + if !detached? + if attaching? + # Let the pending operation complete (#RTL5i) + once_state_changed { transition_state_machine :detaching if can_transition_to?(:detaching) } + elsif can_transition_to?(:detaching) + transition_state_machine :detaching + else + transition_state_machine! :detached + end + end + + deferrable_for_state_change_to(STATE.Detached, &success_block) + end + + # A {Ably::PubSub::Realtime::Presence} object. + # + # @spec RTL9 + # + # @return {Ably::PubSub::Realtime::Presence} + # + def presence + @presence + end + + # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical + # {Ably::Models::Message} objects for the channel. If the channel is configured to persist messages, + # then messages can be retrieved from history for up to 72 hours in the past. If not, messages can only + # be retrieved from history for up to two minutes in the past. + # + # @spec RSL2a + # + # @param (see {Ably::PubSub::Http::Channel#history}) + # @option options (see {Ably::PubSub::Http::Channel#history}) + # @option options [Boolean] :until_attach When true, the history request will be limited only to messages published before this channel was attached. Channel must be attached + # + # @yield [Ably::Models::PaginatedResult] First {Ably::Models::PaginatedResult page} of {Ably::Models::Message} objects accessible with {Ably::Models::PaginatedResult#items #items}. + # + # @return [Ably::Util::SafeDeferrable] + # + def history(options = {}, &callback) + # RTL10b + if options.delete(:until_attach) + unless attached? + error = Ably::Exceptions::InvalidRequest.new('option :until_attach is invalid as the channel is not attached' ) + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + options[:from_serial] = properties.attach_serial + end + + async_wrap(callback) do + rest_channel.history(options.merge(async_blocking_operations: true)) + end + end + + # @return [Ably::Util::PubSub] Client library internal channel incoming message bus + # + # @api private + def __incoming_msgbus__ + @__incoming_msgbus__ ||= Ably::Util::PubSub.new( + coerce_into: lambda { |event| Ably::Models::ProtocolMessage::ACTION(event) } + ) + end + + # Sets the {Ably::Models::ChannelOptions} for the channel. + # An optional callback may be provided to notify of the success or failure of the operation. + # + # @spec RTL16 + # + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} + # @return [Ably::Models::ChannelOptions] + def set_options(channel_options) + @options = Ably::Models::ChannelOptions(channel_options) + # RTL4i + manager.request_reattach if (need_reattach? and connection.state?(:connected)) + end + alias options= set_options + + # @api private + def set_channel_error_reason(error) + @error_reason = error + end + + # @api private + def clear_error_reason + @error_reason = nil + end + + # Used by {Ably::Modules::StateEmitter} to debug state changes + # @api private + def logger + client.logger + end + + # @api private + def attach_resume! + @attach_resume = true + end + + # @api private + def reset_attach_resume! + @attach_resume = false + end + + # As we are using a state machine, do not allow change_state to be used + # #transition_state_machine must be used instead + private :change_state + + def need_reattach? + !!(attaching? || attached?) && !!(options.modes || options.params) + end + + private + + def setup_event_handlers + __incoming_msgbus__.subscribe(:message) do |message| + message.decode(client.encoders, options) do |encode_error, error_message| + client.logger.error error_message + end + emit_message message.name, message + end + end + + def rest_channel + client.rest_client.channel(name) + end + + def connection + client.connection + end + + def setup_presence + @presence ||= Presence.new(self) + end + + # Alias useful for methods with a name argument + def channel_name + name + end + end + end + end +end + +require 'ably/pubsub/realtime/channel/channel_manager' +require 'ably/pubsub/realtime/channel/channel_state_machine' +require 'ably/pubsub/realtime/channel/push_channel' +require 'ably/pubsub/realtime/channel/channel_properties' diff --git a/core/lib/ably/realtime/channel/channel_manager.rb b/core/lib/ably/pubsub/realtime/channel/channel_manager.rb similarity index 99% rename from core/lib/ably/realtime/channel/channel_manager.rb rename to core/lib/ably/pubsub/realtime/channel/channel_manager.rb index 50ccd3168..888c00495 100644 --- a/core/lib/ably/realtime/channel/channel_manager.rb +++ b/core/lib/ably/pubsub/realtime/channel/channel_manager.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Channel # ChannelManager is responsible for all actions relating to channel state: attaching, detaching or failure # Channel state changes are performed by this class and executed from {ChannelStateMachine} @@ -71,7 +71,7 @@ def duplicate_attached_received(protocol_message) channel.emit :update, Ably::Models::ChannelStateChange.new( current: channel.state, previous: channel.state, - event: Ably::Realtime::Channel::EVENT(:update), + event: Ably::PubSub::Realtime::Channel::EVENT(:update), reason: protocol_message.error, resumed: false, ) diff --git a/core/lib/ably/realtime/channel/channel_properties.rb b/core/lib/ably/pubsub/realtime/channel/channel_properties.rb similarity index 71% rename from core/lib/ably/realtime/channel/channel_properties.rb rename to core/lib/ably/pubsub/realtime/channel/channel_properties.rb index 60f1911a8..882a17c52 100644 --- a/core/lib/ably/realtime/channel/channel_properties.rb +++ b/core/lib/ably/pubsub/realtime/channel/channel_properties.rb @@ -1,16 +1,16 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Channel # Describes the properties of the channel state. class ChannelProperties - # {Ably::Realtime::Channel} this object associated with + # {Ably::PubSub::Realtime::Channel} this object associated with # - # @return [Ably::Realtime::Channel] + # @return [Ably::PubSub::Realtime::Channel] # attr_reader :channel # Starts unset when a channel is instantiated, then updated with the channelSerial from each - # {Ably::Realtime::Channel::STATE.Attached} event that matches the channel. - # Used as the value for {Ably::Realtime::Channel#history}. + # {Ably::PubSub::Realtime::Channel::STATE.Attached} event that matches the channel. + # Used as the value for {Ably::PubSub::Realtime::Channel#history}. # # @spec CP2a # diff --git a/core/lib/ably/realtime/channel/channel_state_machine.rb b/core/lib/ably/pubsub/realtime/channel/channel_state_machine.rb similarity index 97% rename from core/lib/ably/realtime/channel/channel_state_machine.rb rename to core/lib/ably/pubsub/realtime/channel/channel_state_machine.rb index e3cedd18c..0a1c96c2e 100644 --- a/core/lib/ably/realtime/channel/channel_state_machine.rb +++ b/core/lib/ably/pubsub/realtime/channel/channel_state_machine.rb @@ -1,8 +1,8 @@ require 'ably/modules/state_machine' -module Ably::Realtime +module Ably::PubSub::Realtime class Channel - # Internal class to manage channel state for {Ably::Realtime::Channel} + # Internal class to manage channel state for {Ably::PubSub::Realtime::Channel} # # @api private # diff --git a/core/lib/ably/realtime/channel/publisher.rb b/core/lib/ably/pubsub/realtime/channel/publisher.rb similarity index 99% rename from core/lib/ably/realtime/channel/publisher.rb rename to core/lib/ably/pubsub/realtime/channel/publisher.rb index 83b3dd85e..12dbfe71a 100644 --- a/core/lib/ably/realtime/channel/publisher.rb +++ b/core/lib/ably/pubsub/realtime/channel/publisher.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Channel # Publisher module adds publishing capabilities to the current object module Publisher diff --git a/core/lib/ably/realtime/channel/push_channel.rb b/core/lib/ably/pubsub/realtime/channel/push_channel.rb similarity index 95% rename from core/lib/ably/realtime/channel/push_channel.rb rename to core/lib/ably/pubsub/realtime/channel/push_channel.rb index eebbf1d71..951cb5cce 100644 --- a/core/lib/ably/realtime/channel/push_channel.rb +++ b/core/lib/ably/pubsub/realtime/channel/push_channel.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Channel # Enables devices to subscribe to push notifications for a channel. # @@ -6,7 +6,7 @@ class PushChannel attr_reader :channel def initialize(channel) - raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::Realtime::Channel) + raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::PubSub::Realtime::Channel) @channel = channel end diff --git a/core/lib/ably/pubsub/realtime/channels.rb b/core/lib/ably/pubsub/realtime/channels.rb new file mode 100644 index 000000000..b05a23ba0 --- /dev/null +++ b/core/lib/ably/pubsub/realtime/channels.rb @@ -0,0 +1,73 @@ +module Ably + module PubSub + module Realtime + # Class that maintains a map of Channels ensuring Channels are reused + class Channels + include Ably::Modules::ChannelsCollection + + # @return [Ably::PubSub::Realtime::Channels] + # + def initialize(client) + super client, Ably::PubSub::Realtime::Channel + end + + # Return a {Ably::PubSub::Realtime::Channel} for the given name + # + # @param name [String] The name of the channel + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} + # + # @return [Ably::PubSub::Realtime::Channel] + # + def get(*args) + super + end + + # Return a {Ably::PubSub::Realtime::Channel} for the given name if it exists, else the block will be called. + # This method is intentionally similar to {http://ruby-doc.org/core-2.1.3/Hash.html#method-i-fetch Hash#fetch} providing a simple way to check if a channel exists or not without creating one + # + # @param name [String] The name of the channel + # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called + # @yieldparam [String] name of the missing channel + # + # @return [Ably::PubSub::Realtime::Channel] + # + def fetch(*args) + super + end + + # Detaches the {Ably::PubSub::Realtime::Channel Realtime Channel} and releases all associated resources. + # + # Releasing a Realtime Channel is not typically necessary as a channel, once detached, consumes no resources other than + # the memory footprint of the {Ably::PubSub::Realtime::Channel Realtime Channel object}. Release channels to free up resources if required + # + # @return [void] + # + def release(channel) + get(channel).detach do + @channels.delete(channel) + end if @channels.has_key?(channel) + end + + # Sets channel serial to each channel from given serials hashmap + # @param [Hash] serials - map of channel name to respective channel serial + # @api private + def set_channel_serials(serials) + serials.each do |channel_name, channel_serial| + get(channel_name).properties.channel_serial = channel_serial + end + end + + # @return [Hash] serials - map of channel name to respective channel serial + # @api private + def get_channel_serials + channel_serials = {} + self.each do |channel| + channel_serials[channel.name] = channel.properties.channel_serial if channel.state == :attached + end + channel_serials + end + + end + end + end +end diff --git a/core/lib/ably/pubsub/realtime/client.rb b/core/lib/ably/pubsub/realtime/client.rb new file mode 100644 index 000000000..377a9299c --- /dev/null +++ b/core/lib/ably/pubsub/realtime/client.rb @@ -0,0 +1,379 @@ +require 'uri' +require 'ably/pubsub/realtime/channel/publisher' +require 'ably/pubsub/realtime/recovery_key_context' + +module Ably + module PubSub + module Realtime + # A client that extends the functionality of the {Ably::PubSub::Realtime::Client} and provides additional realtime-specific features. + # + class Client + include Ably::Modules::AsyncWrapper + include Ably::PubSub::Realtime::Channel::Publisher + include Ably::Modules::Conversions + + extend Forwardable + using Ably::Util::AblyExtensions + + DOMAIN = 'realtime.ably.io' + + # A {Aby::Realtime::Channels} object. + # + # @spec RTC3, RTS1 + # + # @return [Aby::Realtime::Channels] + # + attr_reader :channels + + # An {Ably::Auth} object. + # + # @spec RTC4 + # + # @return [Ably::Auth] + # + attr_reader :auth + + # A {Aby::Realtime::Connection} object. + # + # @spec RTC2 + # + # @return [Aby::Realtime::Connection] + # + attr_reader :connection + + # The {Ably::PubSub::Http::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication + # @return [Ably::PubSub::Http::Client] + + # @private + attr_reader :rest_client + + # When false the client suppresses messages originating from this connection being echoed back on the same connection. Defaults to true + # @return [Boolean] + attr_reader :echo_messages + + # If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states. Defaults to true + # @return [Boolean] + attr_reader :queue_messages + + # The custom realtime websocket host that is being used if it was provided with the option `:ws_host` when the {Client} was created + # @return [String,Nil] + attr_reader :custom_realtime_host + + # When true, as soon as the client library is instantiated it will connect to Ably. If this attribute is false, a connection must be opened explicitly + # @return [Boolean] + attr_reader :auto_connect + + # When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery + # @return [String,Nil] + attr_reader :recover + + # Additional parameters to be sent in the querystring when initiating a realtime connection + # @return [Hash] + attr_reader :transport_params + + def_delegators :auth, :client_id, :auth_options + def_delegators :@rest_client, :encoders + def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary? + def_delegators :@rest_client, :environment, :custom_host, :custom_port, :custom_tls_port + def_delegators :@rest_client, :log_level + def_delegators :@rest_client, :options + + # Refuses direct construction: the package a client is created from is what declares + # the client's side to the platform, and a directly constructed client declares none. + # Use {Ably::PubSub::Server.create_realtime_client} from the +ably-pubsub-server+ gem. + # + # @raise [Ably::Exceptions::DirectConstructionNotSupported] always + def self.new(*args, **kwargs, &block) + raise Ably::Internal.direct_construction_error( + 'Ably::PubSub::Realtime::Client.new', 'Ably::PubSub::Server.create_realtime_client(options)' + ) + end + + # Creates a {Ably::PubSub::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection. + # + # @spec RSC1 + # + # @param (see {Ably::PubSub::Http::Client#initialize}) + # @option options (see Ably::PubSub::Http::Client#initialize) An options {Hash} object. + # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required. + # Whilst the proc is called synchronously, it does not block the EventMachine reactor as it is run in a separate thread. + # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent + # @option options [Boolean] :queue_messages If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states + # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection + # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery + # @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect. + # @option options [Hash] :transport_params Additional parameters to be sent in the querystring when initiating a realtime connection. Keys are Strings, values are Stringifiable(a value must respond to #to_s) + # + # @option options [Integer] :channel_retry_timeout (15 seconds). When a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED + # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in seconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically + # @option options [Integer] :suspended_retry_timeout (30 seconds). When the connection enters the SUSPENDED state, after this delay in seconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically + # @option options [Boolean] :disable_websocket_heartbeats WebSocket heartbeats are more efficient than protocol level heartbeats, however they can be disabled for development purposes + # + # @return [Ably::PubSub::Realtime::Client] + # + # @example + # # Constructs a client using an Ably API key or token string. + # client = Ably::PubSub::Server.create_realtime_client('key.id:secret') + # + # # Constructs a client using an Ably options object. + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret', client_id: 'john') + # + # Applications do not call this constructor: {.new} refuses direct construction, and + # a per-side package reaches it through {Ably::Internal.create_realtime_client}. + # + def initialize(options) + raise ArgumentError, 'Options Hash is expected' if options.nil? + + options = options.clone + if options.kind_of?(String) + options = if options.match(Ably::Auth::API_KEY_REGEX) + { key: options } + else + { token: options } + end + end + + @transport_params = options.delete(:transport_params).to_h.each_with_object({}) do |(key, value), acc| + acc[key.to_s] = value.to_s + end + @rest_client = Ably::Internal.create_http_client(options.merge(realtime_client: self)) + @echo_messages = rest_client.options.fetch_with_default(:echo_messages, true) + @queue_messages = rest_client.options.fetch_with_default(:queue_messages, true) + @custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host] + @auto_connect = rest_client.options.fetch_with_default(:auto_connect, true) + @recover = rest_client.options.fetch_with_default(:recover, '') + + @auth = Ably::PubSub::Realtime::Auth.new(self) + @channels = Ably::PubSub::Realtime::Channels.new(self) + @connection = Ably::PubSub::Realtime::Connection.new(self, options) + + unless @recover.nil_or_empty? + recovery_context = RecoveryKeyContext.from_json(@recover, logger) + unless recovery_context.nil? + @channels.set_channel_serials recovery_context.channel_serials # RTN16j + @connection.set_msg_serial_from_recover = recovery_context.msg_serial # RTN16f + end + end + end + + # Return a {Ably::PubSub::Realtime::Channel Realtime Channel} for the given name + # + # @param (see Ably::PubSub::Realtime::Channels#get) + # @return (see Ably::PubSub::Realtime::Channels#get) + # + def channel(name, channel_options = {}) + channels.get(name, channel_options) + end + + # Retrieves the time from the Ably service as milliseconds since the Unix epoch. Clients that do not have access + # to a sufficiently well maintained time source and wish to issue Ably {Ably::Models::TokenRequests} with + # a more accurate timestamp should use the queryTime property instead of this method. + # + # @spec RTC6a + # + # @yield [Time] The time as milliseconds since the Unix epoch. + # @return [Ably::Util::SafeDeferrable] + # + def time(&success_callback) + async_wrap(success_callback) do + rest_client.time + end + end + + # Queries the REST /stats API and retrieves your application's usage statistics. + # Returns a {Ably::Util::SafeDeferrable} object, containing an array of {Ably::Models::Stats} objects. See the Stats docs. + # + # @spec RTC5 + # + # @param (see Ably::PubSub::Http::Client#stats) + # @option options (see Ably::PubSub::Http::Client#stats) + # + # @yield [Ably::Models::PaginatedResult] A {Ably::Util::SafeDeferrable} object containing an array of {Ably::Models::Stats} objects. + # + # @return [Ably::Util::SafeDeferrable] + # + def stats(options = {}, &success_callback) + async_wrap(success_callback) do + rest_client.stats(options) + end + end + + # Calls {Connection#close} and causes the connection to close, entering the closing state. + # Once closed, the library will not attempt to re-establish the connection without an explicit call to {Connection#connect}. + # @spec RTN12 + # (see Ably::PubSub::Realtime::Connection#close) + def close(&block) + connection.close(&block) + end + + # Calls {Ably::PubSub::Realtime::Connection#connect} and causes the connection to open, entering the connecting + # state. Explicitly calling connect() is unnecessary unless the autoConnect property is disabled. + # @spec RTN11 + # (see Ably::PubSub::Realtime::Connection#connect) + def connect(&block) + connection.connect(&block) + end + + # A {Ably::PubSub::Realtime::Push} object. + # @return [Ably::PubSub::Realtime::Push] + def push + @push ||= Push.new(self) + end + + # Makes a REST request to a provided path. This is provided as a convenience for developers who wish to use REST + # API functionality that is either not documented or is not yet included in the public API, without having to + # directly handle features such as authentication, paging, fallback hosts, MsgPack and JSON support. + # + # @spec RTC9 + # + # (see {Ably::PubSub::Http::Client#request}) + # @yield [Ably::Models::HttpPaginatedResponse<>] An Array of Stats + # + # @return [Ably::Util::SafeDeferrable] An {Ably::Util::SafeDeferrable} response object returned by the HTTP request, containing an empty or JSON-encodable object. + def request(method, path, params = {}, body = nil, headers = {}, &callback) + async_wrap(callback) do + rest_client.request(method, path, params, body, headers, async_blocking_operations: true) + end + end + + # Publish one or more messages to the specified channel. + # + # This method allows messages to be efficiently published to Ably without instancing a {Ably::PubSub::Realtime::Channel} object. + # If you want to publish a high rate of messages to Ably without instancing channels or using the REST API, then this method + # is recommended. However, channel options such as encryption are not supported with this method. If you need to specify channel options + # we recommend you use the {Ably::PubSub::Realtime::Channel} +publish+ method without attaching to each channel, unless you also want to subscribe + # to published messages on that channel. + # + # Note: This feature is still in beta. As such, we cannot guarantee the API will not change in future. + # + # @param channel [String] The channel name you want to publish the message(s) to + # @param name [String, Array, 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 + # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument + # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string + # + # @yield [Ably::Models::Message,Array] On success, will call the block with the {Ably::Models::Message} if a single message is published, or an Array of {Ably::Models::Message} when multiple messages are published + # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks + # + # @example + # # Publish a single message + # client.publish 'activityChannel', click', { x: 1, y: 2 } + # + # # Publish an array of message Hashes + # messages = [ + # { name: 'click', { x: 1, y: 2 } }, + # { name: 'click', { x: 2, y: 3 } } + # ] + # client.publish 'activityChannel', messages + # + # # Publish an array of Ably::Models::Message objects + # messages = [ + # Ably::Models::Message(name: 'click', { x: 1, y: 2 }) + # Ably::Models::Message(name: 'click', { x: 2, y: 3 }) + # ] + # client.publish 'activityChannel', messages + # + # client.publish('activityChannel', 'click', 'body') do |message| + # puts "#{message.name} event received with #{message.data}" + # end + # + # client.publish('activityChannel', 'click', 'body').errback do |error, message| + # puts "#{message.name} was not received, error #{error.message}" + # end + # + def publish(channel_name, name, data = nil, attributes = {}, &success_block) + if !connection.can_publish_messages? + error = Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. 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 + + messages = if name.kind_of?(Enumerable) + name + else + name = ensure_utf_8(:name, name, allow_nil: true) + ensure_supported_payload data + [{ name: name, data: data }.merge(attributes)] + end + + if messages.length > Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE + error = Ably::Exceptions::InvalidRequest.new("It is not possible to publish more than #{Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE} messages with a single publish request.") + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + enqueue_messages_on_connection(self, messages, channel_name).tap do |deferrable| + deferrable.callback(&success_block) if block_given? + end + end + + # @!attribute [r] endpoint + # @return [URI::Generic] Default Ably Realtime endpoint used for all requests + def endpoint + endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-')) + end + + # (see Ably::PubSub::Http::Client#register_encoder) + def register_encoder(encoder) + rest_client.register_encoder encoder + end + + # (see Ably::PubSub::Http::Client#fallback_hosts) + def fallback_hosts + rest_client.fallback_hosts + end + + # (see Ably::PubSub::Http::Client#logger) + def logger + @logger ||= Ably::Logger.new(self, log_level, rest_client.logger.custom_logger) + end + + # Disable connection recovery, typically used after a connection has been recovered + # @return [void] + # @api private + def disable_automatic_connection_recovery + @recover = nil + end + + # @!attribute [r] fallback_endpoint + # @return [URI::Generic] Fallback endpoint used to connect to the realtime Ably service. Note, after each connection attempt, a new random {Ably::FALLBACK_HOSTS fallback host} or provided fallback hosts are used + # @api private + def fallback_endpoint + unless defined?(@fallback_endpoints) && @fallback_endpoints + @fallback_endpoints = fallback_hosts.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) } + @fallback_endpoints << endpoint # Try the original host last if all fallbacks have been used + end + + fallback_endpoint_index = connection.manager.retry_count_for_state(:disconnected) + connection.manager.retry_count_for_state(:suspended) - 1 + + @fallback_endpoints[fallback_endpoint_index % @fallback_endpoints.count] + end + + # Retrieves a {Ably::Models::LocalDevice} object that represents the current state of the device as a target for push notifications. + # @spec RSH8 + # @return [Ably::Models::LocalDevice] A {Ably::Models::LocalDevice} object. + # + # @note This is unsupported in the Ruby library + def device + raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable' + end + + private + def endpoint_for_host(host) + port = if use_tls? + custom_tls_port + else + custom_port + end + + raise ArgumentError, "Custom port must be an Integer or nil" if port && !port.kind_of?(Integer) + + options = { + scheme: use_tls? ? 'wss' : 'ws', + host: host + } + options.merge!(port: port) if port + + URI::Generic.build(options) + end + end + end + end +end diff --git a/core/lib/ably/realtime/client/incoming_message_dispatcher.rb b/core/lib/ably/pubsub/realtime/client/incoming_message_dispatcher.rb similarity index 96% rename from core/lib/ably/realtime/client/incoming_message_dispatcher.rb rename to core/lib/ably/pubsub/realtime/client/incoming_message_dispatcher.rb index 435d70660..8adbcea60 100644 --- a/core/lib/ably/realtime/client/incoming_message_dispatcher.rb +++ b/core/lib/ably/pubsub/realtime/client/incoming_message_dispatcher.rb @@ -1,7 +1,7 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Client # IncomingMessageDispatcher is a (private) class that is used to dispatch {Ably::Models::ProtocolMessage} that are - # received from Ably via the {Ably::Realtime::Connection} + # received from Ably via the {Ably::PubSub::Realtime::Connection} class IncomingMessageDispatcher ACTION = Ably::Models::ProtocolMessage::ACTION @@ -28,7 +28,7 @@ def channels def get_channel(channel_name) channels.fetch(channel_name) do logger.warn { "Received channel message for non-existent channel" } - Ably::Realtime::Models::NilChannel.new + Ably::PubSub::Realtime::Models::NilChannel.new end end @@ -108,7 +108,7 @@ def dispatch_protocol_message(*args) channel.manager.duplicate_attached_received protocol_message else if channel.failed? - logger.warn "Ably::Realtime::Client::IncomingMessageDispatcher - Received an ATTACHED protocol message for FAILED channel #{channel.name}. Ignoring ATTACHED message" + logger.warn "Ably::PubSub::Realtime::Client::IncomingMessageDispatcher - Received an ATTACHED protocol message for FAILED channel #{channel.name}. Ignoring ATTACHED message" else channel.transition_state_machine :attached, reason: protocol_message.error, resumed: protocol_message.has_channel_resumed_flag?, protocol_message: protocol_message end diff --git a/core/lib/ably/realtime/client/outgoing_message_dispatcher.rb b/core/lib/ably/pubsub/realtime/client/outgoing_message_dispatcher.rb similarity index 97% rename from core/lib/ably/realtime/client/outgoing_message_dispatcher.rb rename to core/lib/ably/pubsub/realtime/client/outgoing_message_dispatcher.rb index 49cdefe6b..521f0452d 100644 --- a/core/lib/ably/realtime/client/outgoing_message_dispatcher.rb +++ b/core/lib/ably/pubsub/realtime/client/outgoing_message_dispatcher.rb @@ -1,7 +1,7 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Client # OutgoingMessageDispatcher is a (private) class that is used to deliver - # outgoing {Ably::Models::ProtocolMessage}s using the {Ably::Realtime::Connection} + # outgoing {Ably::Models::ProtocolMessage}s using the {Ably::PubSub::Realtime::Connection} # when the connection state is capable of delivering messages class OutgoingMessageDispatcher include Ably::Modules::EventMachineHelpers diff --git a/core/lib/ably/pubsub/realtime/connection.rb b/core/lib/ably/pubsub/realtime/connection.rb new file mode 100644 index 000000000..752a195e3 --- /dev/null +++ b/core/lib/ably/pubsub/realtime/connection.rb @@ -0,0 +1,740 @@ +require 'securerandom' + +module Ably + module PubSub + module Realtime + # Enables the management of a connection to Ably. + # + class Connection + include Ably::Modules::EventEmitter + include Ably::Modules::Conversions + include Ably::Modules::SafeYield + extend Ably::Modules::Enum + using Ably::Util::AblyExtensions + + + # The current {Ably::PubSub::Realtime::Connection::STATE} of the connection. + # Describes the realtime [Connection]{@link Connection} object states. + # + # @spec RTN4d + # + # INITIALIZED A connection with this state has been initialized but no connection has yet been attempted. + # CONNECTING A connection attempt has been initiated. The connecting state is entered as soon as the library + # has completed initialization, and is reentered each time connection is re-attempted following disconnection. + # CONNECTED A connection exists and is active. + # DISCONNECTED A temporary failure condition. No current connection exists because there is no network connectivity + # or no host is available. The disconnected state is entered if an established connection is dropped, + # or if a connection attempt was unsuccessful. In the disconnected state the library will periodically + # attempt to open a new connection (approximately every 15 seconds), anticipating that the connection + # will be re-established soon and thus connection and channel continuity will be possible. + # In this state, developers can continue to publish messages as they are automatically placed + # in a local queue, to be sent as soon as a connection is reestablished. Messages published by + # other clients while this client is disconnected will be delivered to it upon reconnection, + # so long as the connection was resumed within 2 minutes. After 2 minutes have elapsed, recovery + # is no longer possible and the connection will move to the SUSPENDED state. + # SUSPENDED A long term failure condition. No current connection exists because there is no network connectivity + # or no host is available. The suspended state is entered after a failed connection attempt if + # there has then been no connection for a period of two minutes. In the suspended state, the library + # will periodically attempt to open a new connection every 30 seconds. Developers are unable to + # publish messages in this state. A new connection attempt can also be triggered by an explicit + # call to {Ably::PubSub::Realtime::Connection#connect}. Once the connection has been re-established, + # channels will be automatically re-attached. The client has been disconnected for too long for them + # to resume from where they left off, so if it wants to catch up on messages published by other clients + # while it was disconnected, it needs to use the History API. + # CLOSING An explicit request by the developer to close the connection has been sent to the Ably service. + # If a reply is not received from Ably within a short period of time, the connection is forcibly + # terminated and the connection state becomes CLOSED. + # CLOSED The connection has been explicitly closed by the client. In the closed state, no reconnection attempts + # are made automatically by the library, and clients may not publish messages. No connection state is + # preserved by the service or by the library. A new connection attempt can be triggered by an explicit + # call to {Ably::PubSub::Realtime::Connection#connect}, which results in a new connection. + # FAILED This state is entered if the client library encounters a failure condition that it cannot recover from. + # This may be a fatal connection error received from the Ably service, for example an attempt to connect + # with an incorrect API key, or a local terminal error, for example the token in use has expired + # and the library does not have any way to renew it. In the failed state, no reconnection attempts + # are made automatically by the library, and clients may not publish messages. A new connection attempt + # can be triggered by an explicit call to {Ably::PubSub::Realtime::Connection#connect}. + # + # @return [Ably::PubSub::Realtime::Connection::STATE] + # + STATE = ruby_enum('STATE', + :initialized, + :connecting, + :connected, + :disconnected, + :suspended, + :closing, + :closed, + :failed + ) + + # Describes the events emitted by a {Ably::PubSub::Realtime::Connection} object. An event is either an UPDATE or a {Ably::PubSub::Realtime::Connection::STATE}. + # + # UPDATE RTN4h An event for changes to connection conditions for which the {Ably::PubSub::Realtime::Connection::STATE} does not change. + # + EVENT = ruby_enum('EVENT', + STATE.to_sym_arr + [:update] + ) + + include Ably::Modules::StateEmitter + include Ably::Modules::UsesStateMachine + ensure_state_machine_emits 'Ably::Models::ConnectionStateChange' + + # Defaults for automatic connection recovery and timeouts + DEFAULTS = { + channel_retry_timeout: 15, # when a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED + disconnected_retry_timeout: 15, # when the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically + suspended_retry_timeout: 30, # when the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically + connection_state_ttl: 120, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected + max_connection_state_ttl: nil, # allow a max TTL to be passed in, usually for CI test purposes thus overiding any connection_state_ttl sent from Ably + realtime_request_timeout: 10, # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage + websocket_heartbeats_disabled: false, + }.freeze + + # Max number of messages to bundle in a single ProtocolMessage + MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50 + + # A unique public identifier for this connection, used to identify this member. + # + # @spec RTN8 + # + # @return [String] + # + attr_reader :id + + # A unique private connection key used to recover or resume a connection, assigned by Ably. + # When recovering a connection explicitly, the recoveryKey is used in the recover client options as it contains + # both the key and the last message serial. This private connection key can also be used by other REST clients + # to publish on behalf of this client. See the publishing over REST on behalf of a realtime client docs for more info. + # + # @spec RTN9 + # + # @return [String] + # + attr_reader :key + + # An {Ably::Models::ErrorInfo} object describing the last error received if a connection failure occurs. + # + # @spec RTN14a + # + # @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException] + # + attr_reader :error_reason + + # Connection details of the currently established connection + # @return [Ably::Models::ConnectionDetails] + attr_reader :details + + # {Ably::PubSub::Realtime::Client} associated with this connection + # @return [Ably::PubSub::Realtime::Client] + attr_reader :client + + # Underlying socket transport used for this connection, for internal use by the client library + # @return [Ably::PubSub::Realtime::Connection::WebsocketTransport] + # @api private + attr_reader :transport + + # The Connection manager responsible for creating, maintaining and closing the connection and underlying transport + # @return [Ably::PubSub::Realtime::Connection::ConnectionManager] + # @api private + attr_reader :manager + + # An internal queue used to manage unsent outgoing messages. You should never interface with this array directly + # @return [Array] + # @api private + attr_reader :__outgoing_message_queue__ + + # An internal queue used to manage sent messages. You should never interface with this array directly + # @return [Array] + # @api private + attr_reader :__pending_message_ack_queue__ + + # Configured recovery and timeout defaults for this {Connection}. + # See the configurable options in {Ably::PubSub::Realtime::Client#initialize}. + # The defaults are immutable + # @return [Hash] + attr_reader :defaults + + # @api public + def initialize(client, options) + @client = client + @__outgoing_message_queue__ = [] + @__pending_message_ack_queue__ = [] + + @defaults = DEFAULTS.dup + options.each do |key, val| + @defaults[key] = val if DEFAULTS.has_key?(key) + end if options.kind_of?(Hash) + @defaults.freeze + + Client::IncomingMessageDispatcher.new client, self + Client::OutgoingMessageDispatcher.new client, self + + @state_machine = ConnectionStateMachine.new(self) + @state = STATE(state_machine.current_state) + @manager = ConnectionManager.new(self) + + @current_host = client.endpoint.host + + reset_client_msg_serial + end + + # Causes the connection to close, entering the {Ably::PubSub::Realtime::Connection::STATE} CLOSING state. + # Once closed, the library does not attempt to re-establish the connection without an explicit call to + # {Ably::PubSub::Realtime::Connection#connect}. + # + # @spec RTN12 + # + # @yield block is called as soon as this connection is in the Closed state + # + # @return [EventMachine::Deferrable] + # + def close(&success_block) + unless closing? || closed? + unless can_transition_to?(:closing) + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:closing)) + end + transition_state_machine :closing + end + deferrable_for_state_change_to(STATE.Closed, &success_block) + end + + # Explicitly calling connect() is unnecessary unless the autoConnect attribute of + # the ClientOptions object is false. Unless already connected or connecting, + # this method causes the connection to open, entering the {Ably::PubSub::Realtime::Connection::STATE} CONNECTING state. + # + # @spec RTC1b, RTN3, RTN11 + # + # @yield block is called as soon as this connection is in the Connected state + # + # @return [EventMachine::Deferrable] + # + def connect(&success_block) + unless connecting? || connected? + unless can_transition_to?(:connecting) + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:connecting)) + end + # If connect called in a suspended block, we want to ensure the other callbacks have finished their work first + EventMachine.next_tick { transition_state_machine :connecting if can_transition_to?(:connecting) } + end + + Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| + deferrable.callback do + yield if block_given? + end + succeed_callback = deferrable.method(:succeed) + fail_callback = deferrable.method(:fail) + + unsafe_once(:connected) do + deferrable.succeed + off(&fail_callback) + end + + unsafe_once(:failed, :closed, :closing) do + deferrable.fail + off(&succeed_callback) + end + end + end + + # When connected, sends a heartbeat ping to the Ably server and executes the callback with any error + # and the response time in milliseconds when a heartbeat ping request is echoed from the server. + # This can be useful for measuring true round-trip latency to the connected Ably server. + # + # @spec RTN13 + # + # @yield [Integer] if a block is passed to this method, then this block will be called once the ping heartbeat is received with the time elapsed in seconds. + # If the ping is not received within an acceptable timeframe, the block will be called with +nil+ as he first argument + # + # @example + # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') + # client.connection.ping do |elapsed_s| + # puts "Ping took #{elapsed_s}s" + # end + # + # @return [Ably::Util::SafeDeferrable] + # + def ping(&block) + if initialized? || suspended? || closing? || closed? || failed? + error = Ably::Models::ErrorInfo.new(message: "Cannot send a ping when the connection is #{state}", code: Ably::Exceptions::Codes::DISCONNECTED) + return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) + end + + Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| + started = nil + finished = false + ping_id = SecureRandom.hex(16) + heartbeat_action = Ably::Models::ProtocolMessage::ACTION.Heartbeat + + wait_for_ping = lambda do |protocol_message| + next if finished + if protocol_message.action == heartbeat_action && protocol_message.id == ping_id + finished = true + __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping) + time_passed = Time.now.to_f - started.to_f + deferrable.succeed time_passed + safe_yield block, time_passed if block_given? + end + end + + once_or_if(STATE.Connected) do + next if finished + started = Time.now + send_protocol_message action: heartbeat_action.to_i, id: ping_id + __incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping + end + + once_or_if([:suspended, :closing, :closed, :failed]) do + next if finished + finished = true + deferrable.fail Ably::Models::ErrorInfo.new(message: "Ping failed as connection has changed state to #{state}", code: Ably::Exceptions::Codes::DISCONNECTED) + end + + EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do + next if finished + finished = true + __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping) + error_msg = "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s" + logger.warn { error_msg } + deferrable.fail Ably::Models::ErrorInfo.new(message: error_msg, code: Ably::Exceptions::Codes::TIMEOUT_ERROR) + safe_yield block, nil if block_given? + end + end + end + + # @yield [Boolean] True if an internet connection check appears to be up following an HTTP request to a reliable CDN + # @return [EventMachine::Deferrable] + # @api private + def internet_up? + url = "http#{'s' if client.use_tls?}:#{Ably::INTERNET_CHECK.fetch(:url)}" + EventMachine::DefaultDeferrable.new.tap do |deferrable| + EventMachine::AblyHttpRequest::HttpRequest.new(url, tls: { verify_peer: true }).get.tap do |http| + http.errback do + yield false if block_given? + deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED) + end + http.callback do + EventMachine.next_tick do + result = http.response_header.status == 200 && http.response.strip == Ably::INTERNET_CHECK.fetch(:ok_text) + yield result if block_given? + if result + deferrable.succeed + else + deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unexpected response from #{url} (#{http.response_header.status})", 400, Ably::Exceptions::Codes::BAD_REQUEST) + end + end + end + end + end + end + + # The recovery key string can be used by another client to recover this connection's state in the + # recover client options property. See connection state recover options for more information. + # + # @spec RTN16b, RTN16c + # + # @deprecated Use {#create_recovery_key} instead + # + def recovery_key + logger.warn "[DEPRECATION] recovery_key is deprecated, use create_recovery_key method instead" + create_recovery_key + end + + # The recovery key string can be used by another client to recover this connection's state in the recover client + # options property. See connection state recover options for more information. + # + # @spec RTN16g, RTN16c + # + # @return [String] a json string which incorporates the @connectionKey@, the current @msgSerial@ and collection + # of pairs of channel @name@ and current @channelSerial@ for every currently attached channel + def create_recovery_key + if key.nil_or_empty? || state == :closing || state == :closed || state == :failed || state == :suspended + return nil #RTN16g2 + end + RecoveryKeyContext.new(key, client_msg_serial, client.channels.get_channel_serials).to_json + end + + # Following a new connection being made, the connection ID, connection key + # need to match the details provided by the server. + # + # @return [void] + # @api private + def configure_new(connection_id, connection_key) + @id = connection_id + @key = connection_key + end + + # Disable automatic resume of a connection + # @return [void] + # @api private + def reset_resume_info + @key = nil + @id = nil + end + + # @!attribute [r] __outgoing_protocol_msgbus__ + # @return [Ably::Util::PubSub] Client library internal outgoing protocol message bus + # @api private + def __outgoing_protocol_msgbus__ + @__outgoing_protocol_msgbus__ ||= create_pub_sub_message_bus + end + + # @!attribute [r] __incoming_protocol_msgbus__ + # @return [Ably::Util::PubSub] Client library internal incoming protocol message bus + # @api private + def __incoming_protocol_msgbus__ + @__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus + end + + # Determines the correct host name to use for the next connection attempt and updates current_host + # @yield [String] The host name used for this connection, for network connection failures a {Ably::FALLBACK_HOSTS fallback host} is used to route around networking or intermittent problems if an Internet connection is available + # @api private + def determine_host + raise ArgumentError, 'Block required' unless block_given? + + if should_use_fallback_hosts? + internet_up? do |internet_is_up_result| + @current_host = if internet_is_up_result + client.fallback_endpoint.host + else + client.endpoint.host + end + yield current_host + end + else + @current_host = client.endpoint.host + yield current_host + end + end + + # @return [String] The current host that is configured following a call to method {#determine_host} + # @api private + attr_reader :current_host + + # @!attribute [r] port + # @return [Integer] The default port used for this connection + def port + client.use_tls? ? client.custom_tls_port || 443 : client.custom_port || 80 + end + + # @!attribute [r] logger + # @return [Logger] The {Ably::Logger} for this client. + # Configure the log_level with the `:log_level` option, refer to {Ably::PubSub::Realtime::Client#initialize} + def logger + client.logger + end + + # Add protocol message to the outgoing message queue and notify the dispatcher that a message is + # ready to be sent + # + # @param [Ably::Models::ProtocolMessage] protocol_message + # @return [void] + # @api private + def send_protocol_message(protocol_message) + add_message_serial_if_ack_required_to(protocol_message) do + message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger) + add_message_to_outgoing_queue(message) + notify_message_dispatcher_of_new_message message + end + end + + def send_protocol_message_immediately(protocol_message) + message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger) + add_message_to_outgoing_queue(message, true) + notify_message_dispatcher_of_new_message message + end + + # @api private + def add_message_to_outgoing_queue(protocol_message, send_immediately = false) + if send_immediately + # Adding msg at the top of the queue to get processed immediately while connection is CONNECTED + __outgoing_message_queue__.prepend(protocol_message) + logger.debug { "Connection: protocol msg pushed at the top =>: #{protocol_message.action} #{protocol_message}" } + else + __outgoing_message_queue__ << protocol_message + logger.debug { "Connection: protocol msg queued =>: #{protocol_message.action} #{protocol_message}" } + end + end + + # @api private + def notify_message_dispatcher_of_new_message(protocol_message) + __outgoing_protocol_msgbus__.publish :protocol_message, protocol_message + end + + # @return [EventMachine::Deferrable] + # @api private + def create_websocket_transport + EventMachine::DefaultDeferrable.new.tap do |websocket_deferrable| + # Getting auth params can be blocking so uses a Deferrable + client.auth.auth_params.tap do |auth_deferrable| + auth_deferrable.callback do |auth_params| + url_params = auth_params.merge( + 'format' => client.protocol, + 'echo' => client.echo_messages, + 'v' => Ably::PROTOCOL_VERSION, # RSC7a + 'agent' => client.rest_client.agent + ) + + # Use native websocket heartbeats if possible, but allow Ably protocol heartbeats + url_params['heartbeats'] = if defaults.fetch(:websocket_heartbeats_disabled) + 'true' + else + 'false' + end + # RSA7e1 + url_params['clientId'] = client.auth.client_id_for_request_sync if client.auth.client_id_for_request_sync + url_params.merge!(client.transport_params) + + if !key.nil_or_empty? and connection_state_available? + url_params.merge! resume: key + logger.debug { "Resuming connection with key #{key}" } + elsif !client.recover.nil_or_empty? + recovery_context = RecoveryKeyContext.from_json(client.recover, logger) + unless recovery_context.nil? + key = recovery_context.connection_key + logger.debug { "Recovering connection with key #{key}" } + url_params.merge! recover: key + end + end + + url = URI(client.endpoint).tap do |endpoint| + endpoint.query = URI.encode_www_form(url_params) + end + + determine_host do |host| + # Ensure the hostname matches the fallback host name + url.hostname = host + url.port = port + + begin + logger.debug { "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}" } + @transport = create_transport(host, port, url) do |websocket_transport| + websocket_deferrable.succeed websocket_transport + end + rescue EventMachine::ConnectionError => error + websocket_deferrable.fail error + end + end + end + + auth_deferrable.errback do |error| + websocket_deferrable.fail error + end + end + end + end + + # @api private + def release_websocket_transport + @transport = nil + end + + # @api private + def set_failed_connection_error_reason(error) + @error_reason = error + end + + # @api private + def clear_error_reason + @error_reason = nil + end + + # @api private + def set_connection_details(connection_details) + @details = connection_details + end + + # Returns false if messages cannot be published as a result of message queueing being disabled + # @api private + def can_publish_messages? + connected? || + ( (initialized? || connecting? || disconnected?) && client.queue_messages ) + end + + # @api private + def create_transport(host, port, url, &block) + logger.debug { "Connection: EventMachine connecting to #{host}:#{port} with URL: #{url}" } + EventMachine.connect(host, port, WebsocketTransport, self, url.to_s, &block) + end + + # @api private + def connection_state_ttl + defaults[:max_connection_state_ttl] || # undocumented max TTL configuration + (details && details.connection_state_ttl) || + defaults.fetch(:connection_state_ttl) + end + + def connection_state_ttl=(val) + @connection_state_ttl = val + end + + # @api private + def heartbeat_interval + # See RTN23a + (details && details.max_idle_interval).to_i + + defaults.fetch(:realtime_request_timeout) + end + + # Resets the client message serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage} + # (see #client_msg_serial) + # @api private + def reset_client_msg_serial + @client_msg_serial = -1 + end + + # Sets the client message serial from recover clientOption. + # @api private + def set_msg_serial_from_recover=(value) + @client_msg_serial = value + end + + # When a hearbeat or any other message from Ably is received + # we know it's alive, see #RTN23 + # @api private + def set_connection_confirmed_alive + @last_liveness_event = Time.now + manager.reset_liveness_timer + end + + # @api private + def time_since_connection_confirmed_alive? + Time.now.to_i - @last_liveness_event.to_i + end + + # As we are using a state machine, do not allow change_state to be used + # #transition_state_machine must be used instead + private :change_state + + private + + # The client message serial (msgSerial) is incremented for every message that is published that requires an ACK. + # A message serial number does not guarantee a message has been received, only sent. + # @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent + def client_msg_serial + @client_msg_serial + end + + def create_pub_sub_message_bus + Ably::Util::PubSub.new( + coerce_into: lambda do |event| + raise KeyError, "Expected :protocol_message, :#{event} is disallowed" unless event == :protocol_message + :protocol_message + end + ) + end + + def add_message_serial_if_ack_required_to(protocol_message) + if Ably::Models::ProtocolMessage.ack_required?(protocol_message[:action]) + add_message_serial_to(protocol_message) { yield } + else + yield + end + end + + def add_message_serial_to(protocol_message) + @client_msg_serial += 1 + protocol_message[:msgSerial] = client_msg_serial + yield + rescue StandardError => e + @client_msg_serial -= 1 + raise e + end + + # Simply wait until the next EventMachine tick to ensure Connection initialization is complete + def when_initialized + EventMachine.next_tick { yield } + end + + def connection_state_available? + return true if connected? + + return false if time_since_connection_confirmed_alive? > connection_state_ttl + details.max_idle_interval + + connected_last = state_history.reverse.find { |connected| connected.fetch(:state) == :connected } + if connected_last.nil? + false + else + true + end + end + + def production? + client.environment.nil? || client.environment == :production + end + + def custom_port? + if client.use_tls? + !!client.custom_tls_port + else + !!client.custom_port + end + end + + def custom_host? + !!client.custom_realtime_host + end + + def should_use_fallback_hosts? + if client.fallback_hosts && !client.fallback_hosts.empty? + if connecting? && previous_state && !disconnected_from_connected_state? + use_fallback_if_disconnected? || use_fallback_if_suspended? + end + end + end + + def disconnected_from_connected_state? + most_recent_state_changes = state_history.last(3).first(2) # Ignore current state + + # A valid connection was disconnected + most_recent_state_changes.last.fetch(:state) == Connection::STATE.Disconnected && + most_recent_state_changes.first.fetch(:state) == Connection::STATE.Connected + end + + def use_fallback_if_disconnected? + unless second_reconnect_attempt_for(:disconnected, 1) + return false + end + + does_error_necessitate_fallback(reason_for_last_time_in(:disconnected)) + end + + def use_fallback_if_suspended? + unless second_reconnect_attempt_for(:suspended, 2) # on first suspended state use default Ably host again + return false + end + + does_error_necessitate_fallback(reason_for_last_time_in(:suspended)) + end + + def second_reconnect_attempt_for(state, first_attempt_count) + previous_state == state && manager.retry_count_for_state(state) >= first_attempt_count + end + + # Provides a partial implementation of RTN17f's logic for whether an error necessitates a fallback host. + def does_error_necessitate_fallback(error) + return false unless error + + # For now we just explicitly exclude token errors. TODO: implement properly in https://github.com/ably/ably-ruby/issues/444 + + if error.respond_to?(:status_code) && error.status_code == 401 && error.respond_to?(:code) && Ably::Exceptions::TOKEN_EXPIRED_CODE.include?(error.code) + return false + end + + true + end + + # Returns the error associated with the last state change to the given state (e.g. :disconnected). + def reason_for_last_time_in(state) + history_item = state_history.reverse.find do |history_item| + history_item.fetch(:state) == state + end.fetch(:metadata).reason + end + end + end + end +end + +require 'ably/pubsub/realtime/connection/connection_manager' +require 'ably/pubsub/realtime/connection/connection_state_machine' +require 'ably/pubsub/realtime/connection/websocket_transport' +require 'ably/pubsub/realtime/recovery_key_context' diff --git a/core/lib/ably/realtime/connection/connection_manager.rb b/core/lib/ably/pubsub/realtime/connection/connection_manager.rb similarity index 97% rename from core/lib/ably/realtime/connection/connection_manager.rb rename to core/lib/ably/pubsub/realtime/connection/connection_manager.rb index 5c2d2ab8b..1b5a304a5 100644 --- a/core/lib/ably/realtime/connection/connection_manager.rb +++ b/core/lib/ably/pubsub/realtime/connection/connection_manager.rb @@ -1,6 +1,6 @@ -require 'ably/rest/middleware/exceptions' +require 'ably/pubsub/http/middleware/exceptions' -module Ably::Realtime +module Ably::PubSub::Realtime class Connection # ConnectionManager is responsible for all actions relating to underlying connection and transports, # such as opening, closing, attempting reconnects etc. @@ -35,9 +35,9 @@ def initialize(connection) end end - # Creates and sets up a new {Ably::Realtime::Connection::WebsocketTransport} available on attribute #transport + # Creates and sets up a new {Ably::PubSub::Realtime::Connection::WebsocketTransport} available on attribute #transport # - # @yield [Ably::Realtime::Connection::WebsocketTransport] block is called with new websocket transport + # @yield [Ably::PubSub::Realtime::Connection::WebsocketTransport] block is called with new websocket transport # @api private def setup_transport if transport && !transport.ready_for_release? @@ -151,7 +151,7 @@ def connected_update(protocol_message) state_change = Ably::Models::ConnectionStateChange.new( current: connection.state, previous: connection.state, - event: Ably::Realtime::Connection::EVENT(:update), + event: Ably::PubSub::Realtime::Connection::EVENT(:update), reason: protocol_message.error, protocol_message: protocol_message ) @@ -175,7 +175,7 @@ def release_and_establish_new_transport setup_transport end - # Reconnect the {Ably::Realtime::Connection::WebsocketTransport} if possible, otherwise set up a new transport + # Reconnect the {Ably::PubSub::Realtime::Connection::WebsocketTransport} if possible, otherwise set up a new transport # # @api private def reconnect_transport diff --git a/core/lib/ably/realtime/connection/connection_state_machine.rb b/core/lib/ably/pubsub/realtime/connection/connection_state_machine.rb similarity index 98% rename from core/lib/ably/realtime/connection/connection_state_machine.rb rename to core/lib/ably/pubsub/realtime/connection/connection_state_machine.rb index e3650efaa..cc81f6da8 100644 --- a/core/lib/ably/realtime/connection/connection_state_machine.rb +++ b/core/lib/ably/pubsub/realtime/connection/connection_state_machine.rb @@ -1,8 +1,8 @@ require 'ably/modules/state_machine' -module Ably::Realtime +module Ably::PubSub::Realtime class Connection - # Internal class to manage connection state, recovery and state transitions for {Ably::Realtime::Connection} + # Internal class to manage connection state, recovery and state transitions for {Ably::PubSub::Realtime::Connection} class ConnectionStateMachine include Ably::Modules::StateMachine diff --git a/core/lib/ably/realtime/connection/websocket_transport.rb b/core/lib/ably/pubsub/realtime/connection/websocket_transport.rb similarity index 99% rename from core/lib/ably/realtime/connection/websocket_transport.rb rename to core/lib/ably/pubsub/realtime/connection/websocket_transport.rb index fc50dd190..2d99563c6 100644 --- a/core/lib/ably/realtime/connection/websocket_transport.rb +++ b/core/lib/ably/pubsub/realtime/connection/websocket_transport.rb @@ -1,6 +1,6 @@ require 'openssl' -module Ably::Realtime +module Ably::PubSub::Realtime class Connection # EventMachine WebSocket transport # @api private diff --git a/core/lib/ably/realtime/models/nil_channel.rb b/core/lib/ably/pubsub/realtime/models/nil_channel.rb similarity index 76% rename from core/lib/ably/realtime/models/nil_channel.rb rename to core/lib/ably/pubsub/realtime/models/nil_channel.rb index d1da009a6..4d944778d 100644 --- a/core/lib/ably/realtime/models/nil_channel.rb +++ b/core/lib/ably/pubsub/realtime/models/nil_channel.rb @@ -1,17 +1,17 @@ -module Ably::Realtime::Models +module Ably::PubSub::Realtime::Models # Nil object for Channels, this object is only used within the internal API of this client library # @api private class NilChannel include Ably::Modules::EventEmitter extend Ably::Modules::Enum - STATE = ruby_enum('STATE', Ably::Realtime::Channel::STATE) + STATE = ruby_enum('STATE', Ably::PubSub::Realtime::Channel::STATE) include Ably::Modules::StateEmitter include Ably::Modules::UsesStateMachine attr_reader :state_machine def initialize - @state_machine = Ably::Realtime::Channel::ChannelStateMachine.new(self) + @state_machine = Ably::PubSub::Realtime::Channel::ChannelStateMachine.new(self) @state = STATE(state_machine.current_state) end diff --git a/core/lib/ably/realtime/presence.rb b/core/lib/ably/pubsub/realtime/presence.rb similarity index 91% rename from core/lib/ably/realtime/presence.rb rename to core/lib/ably/pubsub/realtime/presence.rb index 5d66b7e99..5de4f07ef 100644 --- a/core/lib/ably/realtime/presence.rb +++ b/core/lib/ably/pubsub/realtime/presence.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime # Enables the presence set to be entered and subscribed to, and the historic presence set to be retrieved for a channel. # class Presence @@ -19,8 +19,8 @@ class Presence include Ably::Modules::StateEmitter include Ably::Modules::UsesStateMachine - # {Ably::Realtime::Channel} this Presence object is associated with - # @return [Ably::Realtime::Channel] + # {Ably::PubSub::Realtime::Channel} this Presence object is associated with + # @return [Ably::PubSub::Realtime::Channel] attr_reader :channel # The client_id for the member present on this channel @@ -37,7 +37,7 @@ class Presence attr_reader :members # The Presence manager responsible for actions relating to state changes such as entering a channel - # @return [Ably::Realtime::Presence::PresenceManager] + # @return [Ably::PubSub::Realtime::Presence::PresenceManager] # @api private attr_reader :manager @@ -56,7 +56,7 @@ def initialize(channel) # # @param [String,Hash,nil] data optional data (eg a status message) for this member # - # @yield [Ably::Realtime::Presence] On success, will call the block with this {Ably::Realtime::Presence} object + # @yield [Ably::PubSub::Realtime::Presence] On success, will call the block with this {Ably::PubSub::Realtime::Presence} object # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks # def enter(data = nil, &success_block) @@ -100,7 +100,7 @@ def enter(data = nil, &success_block) # @param [String] client_id id of the client # @param [String,Hash,nil] data The payload associated with the presence member. A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. # - # @yield [Ably::Realtime::Presence] On success, will call the block with this {Ably::Realtime::Presence} object + # @yield [Ably::PubSub::Realtime::Presence] On success, will call the block with this {Ably::PubSub::Realtime::Presence} object # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks # def enter_client(client_id, data = nil, &success_block) @@ -165,10 +165,10 @@ def leave(data = nil, &success_block) # # @spec RTP15 # - # @param (see {Ably::Realtime::Presence#enter_client}) + # @param (see {Ably::PubSub::Realtime::Presence#enter_client}) # - # @yield (see {Ably::Realtime::Presence#enter_client}) - # @return (see {Ably::Realtime::Presence#enter_client}) + # @yield (see {Ably::PubSub::Realtime::Presence#enter_client}) + # @return (see {Ably::PubSub::Realtime::Presence#enter_client}) # def leave_client(client_id, data = nil, &success_block) ensure_supported_client_id client_id @@ -178,14 +178,14 @@ def leave_client(client_id, data = nil, &success_block) end # Updates the data payload for a presence member. If called before entering the presence set, this is treated as - # an {Ably::Realtime::Presence::STATE.Entered} event. An optional callback may be provided to notify of the success or failure of the operation. + # an {Ably::PubSub::Realtime::Presence::STATE.Entered} event. An optional callback may be provided to notify of the success or failure of the operation. # # @spec RTP9 # - # @param (see {Ably::Realtime::Presence#enter}) + # @param (see {Ably::PubSub::Realtime::Presence#enter}) # - # @yield (see {Ably::Realtime::Presence#enter}) - # @return (see {Ably::Realtime::Presence#enter}) + # @yield (see {Ably::PubSub::Realtime::Presence#enter}) + # @return (see {Ably::PubSub::Realtime::Presence#enter}) # def update(data = nil, &success_block) deferrable = create_deferrable @@ -233,11 +233,11 @@ def update_client(client_id, data = nil, &success_block) # # @spec RTP11, RTP11c1, RTP11c2, RTP11c3 # - # @param (see {Ably::Realtime::Presence::MembersMap#get}) - # @option options (see {Ably::Realtime::Presence::MembersMap#get}) - # @yield (see {Ably::Realtime::Presence::MembersMap#get}) + # @param (see {Ably::PubSub::Realtime::Presence::MembersMap#get}) + # @option options (see {Ably::PubSub::Realtime::Presence::MembersMap#get}) + # @yield (see {Ably::PubSub::Realtime::Presence::MembersMap#get}) # - # @return (see {Ably::Realtime::Presence::MembersMap#get}) + # @return (see {Ably::PubSub::Realtime::Presence::MembersMap#get}) # def get(options = {}, &block) deferrable = create_deferrable @@ -271,7 +271,7 @@ def get(options = {}, &block) # Registers a listener that is called each time a {Ably::Models::PresenceMessage} is received on the channel, # such as a new member entering the presence set. 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. + # of success or failure of the channel {Ably::PubSub::Realtime::Channel#attach} operation. # # @spec RTP6a, RTP6b # @@ -305,8 +305,8 @@ def unsubscribe(*actions, &callback) # # @spec RTP12c, RTP12a # - # @param (see {Ably::Rest::Presence#history}) - # @option options (see {Ably::Rest::Presence#history}) + # @param (see {Ably::PubSub::Http::Presence#history}) + # @option options (see {Ably::PubSub::Http::Presence#history}) # # @yield [Ably::Models::PaginatedResult] First {Ably::Models::PaginatedResult page} of {Ably::Models::PresenceMessage} objects accessible with {Ably::Models::PaginatedResult#items #items}. # @@ -497,6 +497,6 @@ def create_deferrable end end -require 'ably/realtime/presence/presence_manager' -require 'ably/realtime/presence/members_map' -require 'ably/realtime/presence/presence_state_machine' +require 'ably/pubsub/realtime/presence/presence_manager' +require 'ably/pubsub/realtime/presence/members_map' +require 'ably/pubsub/realtime/presence/presence_state_machine' diff --git a/core/lib/ably/realtime/presence/members_map.rb b/core/lib/ably/pubsub/realtime/presence/members_map.rb similarity index 99% rename from core/lib/ably/realtime/presence/members_map.rb rename to core/lib/ably/pubsub/realtime/presence/members_map.rb index 8183db526..3ceae7bdb 100644 --- a/core/lib/ably/realtime/presence/members_map.rb +++ b/core/lib/ably/pubsub/realtime/presence/members_map.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Presence # A class encapsulating a map of the members of this presence channel, # indexed by the unique {Ably::Models::PresenceMessage#member_key} @@ -164,7 +164,7 @@ def enter_local_members channel.emit :update, Ably::Models::ChannelStateChange.new( current: channel.state, previous: channel.state, - event: Ably::Realtime::Channel::EVENT(:update), + event: Ably::PubSub::Realtime::Channel::EVENT(:update), reason: re_enter_error, resumed: true ) diff --git a/core/lib/ably/realtime/presence/presence_manager.rb b/core/lib/ably/pubsub/realtime/presence/presence_manager.rb similarity index 93% rename from core/lib/ably/realtime/presence/presence_manager.rb rename to core/lib/ably/pubsub/realtime/presence/presence_manager.rb index 3a32a5e0e..5c35f1a85 100644 --- a/core/lib/ably/realtime/presence/presence_manager.rb +++ b/core/lib/ably/pubsub/realtime/presence/presence_manager.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Presence # PresenceManager is responsible for all actions relating to presence state # @@ -9,8 +9,8 @@ class Presence class PresenceManager extend Forwardable - # {Ably::Realtime::Presence} this Manager is associated with - # @return [Ably::Realtime::Presence] + # {Ably::PubSub::Realtime::Presence} this Manager is associated with + # @return [Ably::PubSub::Realtime::Presence] attr_reader :presence def initialize(presence) diff --git a/core/lib/ably/realtime/presence/presence_state_machine.rb b/core/lib/ably/pubsub/realtime/presence/presence_state_machine.rb similarity index 94% rename from core/lib/ably/realtime/presence/presence_state_machine.rb rename to core/lib/ably/pubsub/realtime/presence/presence_state_machine.rb index 582bc2983..bc1d4a495 100644 --- a/core/lib/ably/realtime/presence/presence_state_machine.rb +++ b/core/lib/ably/pubsub/realtime/presence/presence_state_machine.rb @@ -1,8 +1,8 @@ require 'ably/modules/state_machine' -module Ably::Realtime +module Ably::PubSub::Realtime class Presence - # Internal class to manage presence state for {Ably::Realtime::Presence} + # Internal class to manage presence state for {Ably::PubSub::Realtime::Presence} # # @api private # diff --git a/core/lib/ably/pubsub/realtime/push.rb b/core/lib/ably/pubsub/realtime/push.rb new file mode 100644 index 000000000..3a8a13277 --- /dev/null +++ b/core/lib/ably/pubsub/realtime/push.rb @@ -0,0 +1,27 @@ +require 'ably/pubsub/realtime/push/admin' + +module Ably + module PubSub + module Realtime + # Class providing push notification functionality + class Push + # @private + attr_reader :client + + def initialize(client) + @client = client + end + + # A {Ably::PubSub::Realtime::Push::Admin} object. + # + # @spec RSH1 + # + # @return [Ably::PubSub::Realtime::Push::Admin] + # + def admin + @admin ||= Admin.new(self) + end + end + end + end +end diff --git a/core/lib/ably/realtime/push/admin.rb b/core/lib/ably/pubsub/realtime/push/admin.rb similarity index 79% rename from core/lib/ably/realtime/push/admin.rb rename to core/lib/ably/pubsub/realtime/push/admin.rb index 746b87af2..c314df85a 100644 --- a/core/lib/ably/realtime/push/admin.rb +++ b/core/lib/ably/pubsub/realtime/push/admin.rb @@ -1,7 +1,7 @@ -require 'ably/realtime/push/device_registrations' -require 'ably/realtime/push/channel_subscriptions' +require 'ably/pubsub/realtime/push/device_registrations' +require 'ably/pubsub/realtime/push/channel_subscriptions' -module Ably::Realtime +module Ably::PubSub::Realtime class Push # Class providing push notification administrative functionality # for registering devices and attaching to channels etc. @@ -23,7 +23,7 @@ def initialize(push) # Sends a push notification directly to a device, or a group of devices sharing the same clientId. # - # (see Ably::Rest::Push#publish) + # (see Ably::PubSub::Http::Push#publish) # # @spec RSH1a # @@ -42,21 +42,21 @@ def publish(recipient, data, &callback) end end - # A {Ably::Realtime::Push::DeviceRegistrations} object. + # A {Ably::PubSub::Realtime::Push::DeviceRegistrations} object. # # @spec RSH1b # - # @return [Ably::Realtime::Push::DeviceRegistrations] + # @return [Ably::PubSub::Realtime::Push::DeviceRegistrations] # def device_registrations @device_registrations ||= DeviceRegistrations.new(self) end - # A {Ably::Realtime::Push::ChannelSubscriptions} object. + # A {Ably::PubSub::Realtime::Push::ChannelSubscriptions} object. # # @spec RSH1c # - # @return [Ably::Realtime::Push::ChannelSubscriptions] + # @return [Ably::PubSub::Realtime::Push::ChannelSubscriptions] # def channel_subscriptions @channel_subscriptions ||= ChannelSubscriptions.new(self) diff --git a/core/lib/ably/realtime/push/channel_subscriptions.rb b/core/lib/ably/pubsub/realtime/push/channel_subscriptions.rb similarity index 90% rename from core/lib/ably/realtime/push/channel_subscriptions.rb rename to core/lib/ably/pubsub/realtime/push/channel_subscriptions.rb index 3afb51cc3..773b805c2 100644 --- a/core/lib/ably/realtime/push/channel_subscriptions.rb +++ b/core/lib/ably/pubsub/realtime/push/channel_subscriptions.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Push # Manage push notification channel subscriptions for devices or clients class ChannelSubscriptions @@ -16,7 +16,7 @@ def initialize(admin) @client = admin.client end - # (see Ably::Rest::Push::ChannelSubscriptions#list) + # (see Ably::PubSub::Http::Push::ChannelSubscriptions#list) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -33,7 +33,7 @@ def list(params, &callback) end end - # (see Ably::Rest::Push::ChannelSubscriptions#list_channels) + # (see Ably::PubSub::Http::Push::ChannelSubscriptions#list_channels) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -47,7 +47,7 @@ def list_channels(params = {}, &callback) end end - # (see Ably::Rest::Push::ChannelSubscriptions#save) + # (see Ably::PubSub::Http::Push::ChannelSubscriptions#save) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -61,7 +61,7 @@ def save(push_channel_subscription, &callback) end end - # (see Ably::Rest::Push::ChannelSubscriptions#remove) + # (see Ably::PubSub::Http::Push::ChannelSubscriptions#remove) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -78,7 +78,7 @@ def remove(push_channel_subscription, &callback) end end - # (see Ably::Rest::Push::ChannelSubscriptions#remove_where) + # (see Ably::PubSub::Http::Push::ChannelSubscriptions#remove_where) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] diff --git a/core/lib/ably/realtime/push/device_registrations.rb b/core/lib/ably/pubsub/realtime/push/device_registrations.rb similarity index 89% rename from core/lib/ably/realtime/push/device_registrations.rb rename to core/lib/ably/pubsub/realtime/push/device_registrations.rb index 37f22246e..2f18e1679 100644 --- a/core/lib/ably/realtime/push/device_registrations.rb +++ b/core/lib/ably/pubsub/realtime/push/device_registrations.rb @@ -1,4 +1,4 @@ -module Ably::Realtime +module Ably::PubSub::Realtime class Push # Manage device registrations for push notifications class DeviceRegistrations @@ -16,7 +16,7 @@ def initialize(admin) @client = admin.client end - # (see Ably::Rest::Push::DeviceRegistrations#get) + # (see Ably::PubSub::Http::Push::DeviceRegistrations#get) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -30,7 +30,7 @@ def get(device_id, &callback) end end - # (see Ably::Rest::Push::DeviceRegistrations#list) + # (see Ably::PubSub::Http::Push::DeviceRegistrations#list) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -45,7 +45,7 @@ def list(params = {}, &callback) end end - # (see Ably::Rest::Push::DeviceRegistrations#save) + # (see Ably::PubSub::Http::Push::DeviceRegistrations#save) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -59,7 +59,7 @@ def save(device, &callback) end end - # (see Ably::Rest::Push::DeviceRegistrations#remove) + # (see Ably::PubSub::Http::Push::DeviceRegistrations#remove) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] @@ -73,7 +73,7 @@ def remove(device_id, &callback) end end - # (see Ably::Rest::Push::DeviceRegistrations#remove_where) + # (see Ably::PubSub::Http::Push::DeviceRegistrations#remove_where) # # @yield Block is invoked when request succeeds # @return [Ably::Util::SafeDeferrable] diff --git a/core/lib/ably/pubsub/realtime/recovery_key_context.rb b/core/lib/ably/pubsub/realtime/recovery_key_context.rb new file mode 100644 index 000000000..5174b0f7e --- /dev/null +++ b/core/lib/ably/pubsub/realtime/recovery_key_context.rb @@ -0,0 +1,38 @@ +require 'json' +# frozen_string_literal: true + +module Ably + module PubSub + module Realtime + class RecoveryKeyContext + attr_reader :connection_key + attr_reader :msg_serial + attr_reader :channel_serials + + def initialize(connection_key, msg_serial, channel_serials) + @connection_key = connection_key + @msg_serial = msg_serial + @channel_serials = channel_serials + if @channel_serials.nil? + @channel_serials = {} + end + end + + def to_json + { 'connection_key' => @connection_key, 'msg_serial' => @msg_serial, 'channel_serials' => @channel_serials }.to_json + end + + def self.from_json(obj, logger = nil) + begin + data = JSON.load obj + self.new data['connection_key'], data['msg_serial'], data['channel_serials'] + rescue => e + logger.warn "unable to decode recovery key, found error #{e}" unless logger.nil? + return nil + end + end + + end + end + end +end diff --git a/core/lib/ably/realtime.rb b/core/lib/ably/realtime.rb deleted file mode 100644 index f6171e1f1..000000000 --- a/core/lib/ably/realtime.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'eventmachine' -require 'websocket/driver' -require 'em-http-request' - -require 'ably/modules/event_emitter' - -require 'ably/realtime/auth' -require 'ably/realtime/channel' -require 'ably/realtime/channels' -require 'ably/realtime/client' -require 'ably/realtime/connection' -require 'ably/realtime/push' -require 'ably/realtime/presence' - -require 'ably/models/message_encoders/base' - -Dir.glob(File.expand_path("models/*.rb", File.dirname(__FILE__))).each do |file| - require file -end - -Dir.glob(File.expand_path("realtime/models/*.rb", File.dirname(__FILE__))).each do |file| - require file -end - -require 'ably/models/message_encoders/base' - -require 'ably/realtime/client/incoming_message_dispatcher' -require 'ably/realtime/client/outgoing_message_dispatcher' - -module Ably - # Realtime is the namespace of the stateful realtime client and the models it returns. - # - # @example - # client = Ably::PubSub::Server.create_realtime_client("xxxxx") - # channel = client.channel("test") - # channel.subscribe do |message| - # message[:name] #=> "greeting" - # end - # channel.publish "greeting", "data" - # - module Realtime - # Refuses construction. This was a convenience alias for the {Ably::Realtime::Client} - # constructor, which no longer accepts direct construction: the package a client is - # created from is what declares the client's side to the platform, and a client - # constructed here declares none. - # - # Use {Ably::PubSub::Server.create_realtime_client} from the +ably-pubsub-server+ gem. - # - # @raise [Ably::Exceptions::DirectConstructionNotSupported] always - def self.new(*args, **kwargs, &block) - raise Ably::Internal.direct_construction_error( - 'Ably::Realtime.new', 'Ably::PubSub::Server.create_realtime_client(options)' - ) - end - end -end diff --git a/core/lib/ably/realtime/auth.rb b/core/lib/ably/realtime/auth.rb deleted file mode 100644 index 20cb68f09..000000000 --- a/core/lib/ably/realtime/auth.rb +++ /dev/null @@ -1,283 +0,0 @@ -require 'ably/auth' - -module Ably - module Realtime - # Auth is responsible for authentication with {https://www.ably.com Ably} using basic or token authentication - # This {Ably::Realtime::Auth Realtime::Auth} class wraps the {Ably::Auth Synchronous Ably::Auth} class in an EventMachine friendly way using Deferrables for all IO. See {Ably::Auth Ably::Auth} for more information - # - # Find out more about Ably authentication at: https://www.ably.com/docs/general/authentication/ - # - # @!attribute [r] client_id - # (see Ably::Auth#client_id) - # @!attribute [r] current_token_details - # (see Ably::Auth#current_token_details) - # @!attribute [r] token - # (see Ably::Auth#token) - # @!attribute [r] key - # (see Ably::Auth#key) - # @!attribute [r] key_name - # (see Ably::Auth#key_name) - # @!attribute [r] key_secret - # (see Ably::Auth#key_secret) - # @!attribute [r] options - # (see Ably::Auth#options) - # @!attribute [r] token_params - # (see Ably::Auth#options) - # @!attribute [r] using_basic_auth? - # (see Ably::Auth#using_basic_auth?) - # @!attribute [r] using_token_auth? - # (see Ably::Auth#using_token_auth?) - # @!attribute [r] token_renewable? - # (see Ably::Auth#token_renewable?) - # @!attribute [r] authentication_security_requirements_met? - # (see Ably::Auth#authentication_security_requirements_met?) - # - class Auth - extend Forwardable - include Ably::Modules::AsyncWrapper - - def_delegators :auth_sync, :client_id - def_delegators :auth_sync, :token_client_id_allowed?, :configure_client_id, :client_id_validated? - def_delegators :auth_sync, :can_assume_client_id?, :has_client_id? - def_delegators :auth_sync, :current_token_details, :token - def_delegators :auth_sync, :key, :key_name, :key_secret, :options, :auth_options, :token_params - def_delegators :auth_sync, :using_basic_auth?, :using_token_auth? - def_delegators :auth_sync, :token_renewable?, :authentication_security_requirements_met? - def_delegators :client, :logger - def_delegators :client, :connection - - def initialize(client) - @client = client - @auth_sync = client.rest_client.auth - end - - # For new connections, ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. - # If a connection is already established, the connection will be upgraded with a new token - # - # In the event that a new token request is made, the provided options are used - # - # @param (see Ably::Auth#authorize) - # @option (see Ably::Auth#authorize) - # - # @return [Ably::Util::SafeDeferrable] - # @yield [Ably::Models::TokenDetails] - # - # @example - # # will issue a simple token request using basic auth - # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') - # client.auth.authorize do |token_details| - # token_details #=> Ably::Models::TokenDetails - # end - # - def authorize(token_params = nil, auth_options = nil, &success_callback) - Ably::Util::SafeDeferrable.new(logger).tap do |authorize_method_deferrable| - # Wrap the sync authorize method and wait for the result from the deferrable - async_wrap do - authorize_sync(token_params, auth_options) - end.tap do |auth_operation| - # Authorize operation succeeded and we have a new token, now let's perform inline authentication - auth_operation.callback do |token| - case connection.state.to_sym - when :initialized, :disconnected, :suspended, :closed, :closing, :failed - connection.connect - when :connected - perform_inline_auth token - when :connecting - # Fail all current connection attempts and try again with the new token, see #RTC8b - connection.manager.release_and_establish_new_transport - else - logger.fatal { "Auth#authorize: unsupported state #{connection.state}" } - authorize_method_deferrable.fail Ably::Exceptions::InvalidState.new("Unsupported state #{connection.state} for Auth#authorize") - next - end - - # Indicate success or failure based on response from realtime, see #RTC8b1 - auth_deferrable_resolved = false - - connection.unsafe_once(:connected, :update) do - auth_deferrable_resolved = true - authorize_method_deferrable.succeed token - end - connection.unsafe_once(:suspended, :closed, :failed) do |state_change| - auth_deferrable_resolved = true - authorize_method_deferrable.fail state_change.reason - end - end - - # Authorize failed, likely due to auth_url or auth_callback failing - auth_operation.errback do |error| - client.connection.transition_state_machine :failed, reason: error if error.kind_of?(Ably::Exceptions::IncompatibleClientId) - authorize_method_deferrable.fail error - end - end - - # Call the block provided to this method upon success of this deferrable - authorize_method_deferrable.callback do |token| - yield token if block_given? - end - end - end - - # @deprecated Use {#authorize} instead - def authorise(*args, &block) - logger.warn { "Auth#authorise is deprecated and will be removed in 1.0. Please use Auth#authorize instead" } - authorize(*args, &block) - end - - # Synchronous version of {#authorize}. See {Ably::Auth#authorize} for method definition - # Please note that authorize_sync will however not upgrade the current connection's token as this requires - # an synchronous operation to send the new authentication details to Ably over a realtime connection - # - # @param (see Ably::Auth#authorize) - # @option (see Ably::Auth#authorize) - # @return [Ably::Models::TokenDetails] - # - def authorize_sync(token_params = nil, auth_options = nil) - @authorization_in_flight = true - auth_sync.authorize(token_params, auth_options) - ensure - @authorization_in_flight = false - end - - # @api private - def authorization_in_flight? - @authorization_in_flight - end - - # @deprecated Use {#authorize_sync} instead - def authorise_sync(*args) - logger.warn { "Auth#authorise_sync is deprecated and will be removed in 1.0. Please use Auth#authorize_sync instead" } - authorize_sync(*args) - end - - # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests - # - # @param (see Ably::Auth#request_token) - # @option (see Ably::Auth#request_token) - # - # @return [Ably::Util::SafeDeferrable] - # @yield [Ably::Models::TokenDetails] - # - # @example - # # simple token request using basic auth - # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') - # client.auth.request_token do |token_details| - # token_details #=> Ably::Models::TokenDetails - # end - # - def request_token(token_params = {}, auth_options = {}, &success_callback) - async_wrap(success_callback) do - request_token_sync(token_params, auth_options) - end - end - - # Synchronous version of {#request_token}. See {Ably::Auth#request_token} for method definition - # @param (see Ably::Auth#authorize) - # @option (see Ably::Auth#authorize) - # @return [Ably::Models::TokenDetails] - # - def request_token_sync(token_params = {}, auth_options = {}) - auth_sync.request_token(token_params, auth_options) - end - - # Creates and signs a token request that can then subsequently be used by any client to request a token - # - # @param (see Ably::Auth#create_token_request) - # @option (see Ably::Auth#create_token_request) - # - # @return [Ably::Util::SafeDeferrable] - # @yield [Models::TokenRequest] - # - # @example - # client.auth.create_token_request({ ttl: 3600 }, id: 'asd.asd') do |token_request| - # token_request #=> Ably::Models::TokenRequest - # end - def create_token_request(token_params = {}, auth_options = {}, &success_callback) - async_wrap(success_callback) do - create_token_request_sync(token_params, auth_options) - end - end - - # Synchronous version of {#create_token_request}. See {Ably::Auth#create_token_request} for method definition - # @param (see Ably::Auth#authorize) - # @option (see Ably::Auth#authorize) - # @return [Ably::Models::TokenRequest] - # - def create_token_request_sync(token_params = {}, auth_options = {}) - auth_sync.create_token_request(token_params, auth_options) - end - - # Auth header string used in HTTP requests to Ably - # Will reauthorize implicitly if required and capable - # - # @return [Ably::Util::SafeDeferrable] - # @yield [String] HTTP authentication value used in HTTP_AUTHORIZATION header - # - def auth_header(&success_callback) - async_wrap(success_callback) do - auth_header_sync - end - end - - # Synchronous version of {#auth_header}. See {Ably::Auth#auth_header} for method definition - # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header - # - def auth_header_sync - auth_sync.auth_header - end - - def client_id_for_request_sync - auth_sync.client_id_for_request - end - - # Auth params used in URI endpoint for Realtime connections - # Will reauthorize implicitly if required and capable - # - # @return [Ably::Util::SafeDeferrable] - # @yield [Hash] Auth params for a new Realtime connection - # - def auth_params(&success_callback) - fail_callback = lambda do |error, deferrable| - logger.error { "Failed to authenticate: #{error}" } - if error.kind_of?(Ably::Exceptions::BaseAblyException) - # Use base exception if it exists carrying forward the status codes - deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, nil, nil, error) - else - deferrable.fail Ably::Exceptions::AuthenticationFailed.new(error.message, 500, Ably::Exceptions::Codes::CLIENT_CONFIGURED_AUTHENTICATION_PROVIDER_REQUEST_FAILED) - end - end - async_wrap(success_callback, fail_callback) do - auth_params_sync - end - end - - # Synchronous version of {#auth_params}. See {Ably::Auth#auth_params} for method definition - # @return [Hash] Auth params for a new Realtime connection - # - def auth_params_sync - auth_sync.auth_params - end - - private - # The synchronous Auth class instanced by the Rest client - # @return [Ably::Auth] - def auth_sync - @auth_sync - end - - def client - @client - end - - # Sends an AUTH ProtocolMessage on the existing connection triggering - # an inline AUTH process, see #RTC8a - def perform_inline_auth(token) - logger.debug { "Performing inline AUTH with Ably using token #{token}" } - connection.send_protocol_message( - action: Ably::Models::ProtocolMessage::ACTION.Auth.to_i, - auth: { access_token: token.token } - ) - end - end - end -end diff --git a/core/lib/ably/realtime/channel.rb b/core/lib/ably/realtime/channel.rb deleted file mode 100644 index af1772c0f..000000000 --- a/core/lib/ably/realtime/channel.rb +++ /dev/null @@ -1,440 +0,0 @@ -require 'ably/realtime/channel/publisher' - -module Ably - module Realtime - # Enables messages to be published and subscribed to. Also enables historic messages to be retrieved and provides - # access to the {Ably::Realtime::Channel} object of a channel. - # - # Channels will always be in one of the following states: - # - # initialized: 0 - # attaching: 1 - # attached: 2 - # detaching: 3 - # detached: 4 - # failed: 5 - # - # Note that the states are available as Enum-like constants: - # - # Channel::STATE.Initialized The channel has been initialized but no attach has yet been attempted. - # Channel::STATE.Attaching An attach has been initiated by sending a request to Ably. - # This is a transient state, followed either by a transition to ATTACHED, SUSPENDED, or FAILED. - # Channel::STATE.Attached The attach has succeeded. In the ATTACHED state a client may publish and subscribe to messages, or be present on the channel. - # Channel::STATE.Detaching A detach has been initiated on an ATTACHED channel by sending a request to Ably. - # This is a transient state, followed either by a transition to DETACHED or FAILED. - # Channel::STATE.Detached The channel, having previously been ATTACHED, has been detached by the user. - # Channel::STATE.Suspended The channel, having previously been ATTACHED, has lost continuity, usually due to - # the client being disconnected from Ably for longer than two minutes. It will automatically attempt to reattach as soon as connectivity is restored. - # Channel::STATE.Failed An indefinite failure condition. This state is entered if a channel error - # has been received from the Ably service, such as an attempt to attach without the necessary access rights. - # - class Channel - include Ably::Modules::Conversions - include Ably::Modules::EventEmitter - include Ably::Modules::EventMachineHelpers - include Ably::Modules::AsyncWrapper - include Ably::Modules::MessageEmitter - include Ably::Realtime::Channel::Publisher - extend Ably::Modules::Enum - extend Forwardable - - # The current {Abbly::Realtime::Channel::STATE} of the channel. - # - # @spec RTL2b - # - # The permitted states for this channel - STATE = ruby_enum('STATE', - :initialized, - :attaching, - :attached, - :detaching, - :detached, - :suspended, - :failed - ) - - # Describes the events emitted by a {Ably::Rest::Channel} or {Ably::Realtime::Channel} object. - # An event is either an UPDATE or a {Ably::Rest::Channel::STATE}. - # - # The permitted channel events that are emitted for this channel - # - # @spec RTL2g - # - EVENT = ruby_enum('EVENT', - STATE.to_sym_arr + [:update] - ) - - include Ably::Modules::StateEmitter - include Ably::Modules::UsesStateMachine - ensure_state_machine_emits 'Ably::Models::ChannelStateChange' - - # Max number of messages to bundle in a single ProtocolMessage - MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50 - - # {Ably::Realtime::Client} associated with this channel - # - # @return [Ably::Realtime::Client] - # - # @api private - attr_reader :client - - # The channel name. - # @return [String] - attr_reader :name - - # A {Ably::Realtime::Channel::PushChannel} object. - # - # @return [Ably::Realtime::Channel::PushChannel] - attr_reader :push - - # Channel options configured for this channel, see {#initialize} for channel_options - # @return [Hash] - attr_reader :options - - # A {Ably::Realtime::Channel::ChannelProperties} object. - # - # @spec CP1, RTL15 - # - # @return [{Ably::Realtime::Channel::ChannelProperties}] - attr_reader :properties - - # An {Ably::Models::ErrorInfo} object describing the last error which occurred on the channel, if any. - # @spec RTL4e - # @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException] - attr_reader :error_reason - - # The Channel manager responsible for attaching, detaching and handling failures for this channel - # @return [Ably::Realtime::Channel::ChannelManager] - # @api private - attr_reader :manager - - # Flag that specifies whether channel is resuming attachment(reattach) or is doing a 'clean attach' RTL4j1 - # @return [Boolean] - # @api private - attr_reader :attach_resume - - # Optional channel parameters that configure the behavior of the channel. - # @spec RTL4k1 - # return [Hash] - def_delegators :options, :params - - # Initialize a new Channel object - # - # @param client [Ably::Rest::Client] - # @param name [String] The name of the channel - # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} - # - def initialize(client, name, channel_options = {}) - name = ensure_utf_8(:name, name) - - @options = Ably::Models::ChannelOptions(channel_options) - @client = client - @name = name - @queue = [] - - @state_machine = ChannelStateMachine.new(self) - @state = STATE(state_machine.current_state) - @manager = ChannelManager.new(self, client.connection) - @push = PushChannel.new(self) - @properties = ChannelProperties.new(self) - @attach_resume = false - - setup_event_handlers - setup_presence - end - - # Publish a message to the channel. A callback may optionally be passed in to this call to be notified of success - # or failure of the operation. When publish is called with this client library, it won't attempt to implicitly - # attach to the channel. - # - # @spec RTL6i - # - # @param name [String, Array, 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 - # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument - # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string - # - # @yield [Ably::Models::Message,Array] On success, will call the block with the {Ably::Models::Message} if a single message is published, or an Array of {Ably::Models::Message} when multiple messages are published - # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks - # - # @example - # # Publish a single message form - # channel.publish 'click', { x: 1, y: 2 } - # - # # Publish a single message with single Hash form - # message = { name: 'click', data: { x: 1, y: 2 } } - # channel.publish message - # - # # Publish an array of message Hashes form - # messages = [ - # { name: 'click', data: { x: 1, y: 2 } }, - # { name: 'click', data: { x: 2, y: 3 } } - # ] - # channel.publish messages - # - # # Publish an array of Ably::Models::Message objects form - # messages = [ - # Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) - # Ably::Models::Message(name: 'click', data: { x: 2, y: 3 }) - # ] - # channel.publish messages - # - # # Publish an array of Ably::Models::Message objects form - # message = Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) - # channel.publish message - # - # channel.publish('click', 'body') do |message| - # puts "#{message.name} event received with #{message.data}" - # end - # - # channel.publish('click', 'body').errback do |error, message| - # puts "#{message.name} was not received, error #{error.message}" - # end - # - def publish(name, data = nil, attributes = {}, &success_block) - if suspended? || failed? - error = Ably::Exceptions::ChannelInactive.new("Cannot publish messages on a channel in state #{state}") - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - if !connection.can_publish_messages? - error = Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. 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 - - messages = build_messages(name, data, attributes) # (RSL1a, RSL1b) - - if messages.length > Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE - error = Ably::Exceptions::InvalidRequest.new("It is not possible to publish more than #{Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE} messages with a single publish request.") - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - enqueue_messages_on_connection(client, messages, channel_name, options).tap do |deferrable| - deferrable.callback(&success_block) if block_given? - end - end - - # Registers a listener for messages on this channel. The caller supplies a listener function, which is called - # each time one or more messages arrives on the channel. A callback may optionally be passed in to this call - # to be notified of success or failure of the channel {Ably::Realtime::Channel#attach} operation. - # - # @spec RTL7a - # - # @param names [String] The event name of the message to subscribe to if provided. Defaults to all events. - # @yield [Ably::Models::Message] For each message received, the block is called - # - # @return [void] - # - def subscribe(*names, &callback) - attach unless attached? || attaching? - super - end - - # Deregisters the given listener for the specified event name(s). This removes an earlier event-specific subscription. - # - # @spec RTL8a - # - # @param names [String] The event name of the message to subscribe to if provided. Defaults to all events. - # - # @return [void] - # - def unsubscribe(*names, &callback) - super - end - - # Attach to this channel ensuring the channel is created in the Ably system and all messages published on - # the channel are received by any channel listeners registered using {Ably::Realtime::Channel#subscribe}. - # Any resulting channel state change will be emitted to any listeners registered using the {Ably::Modules::EventEmitter#on} - # or {Ably::Modules::EventEmitter#once} methods. A callback may optionally be passed in to this call to be notified - # of success or failure of the operation. As a convenience, attach() is called implicitly - # if {Ably::Realtime::Channel#subscribe} for the channel is called, or {Ably::Realtime::Presence#enter} - # or {Ably::Realtime::Presence#subscribe} are called on the {Ably::Realtime::Presence} object for this channel. - # - # @spec RTL4d - # - # @yield [Ably::Realtime::Channel] Block is called as soon as this channel is in the Attached state - # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback - # - def attach(&success_block) - if connection.closing? || connection.closed? || connection.suspended? || connection.failed? - error = Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}") - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - if !attached? - if detaching? - # Let the pending operation complete (#RTL4h) - once_state_changed { transition_state_machine :attaching if can_transition_to?(:attaching) } - else - transition_state_machine :attaching if can_transition_to?(:attaching) - end - end - - deferrable_for_state_change_to(STATE.Attached, &success_block) - end - - # Detach from this channel. Any resulting channel state change is emitted to any listeners registered using - # the {Ably::Modules::EventEmitter#on} or {Ably::Modules::EventEmitter#once} methods. A callback may optionally - # be passed in to this call to be notified of success or failure of the operation. Once all clients globally - # have detached from the channel, the channel will be released in the Ably service within two minutes. - # - # @spec RTL5e - # - # @yield [Ably::Realtime::Channel] Block is called as soon as this channel is in the Detached or Failed state - # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback - # - def detach(&success_block) - if initialized? - success_block.call if block_given? - return Ably::Util::SafeDeferrable.new_and_succeed_immediately(logger) - end - - if failed? || connection.closing? || connection.failed? - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:detaching)) - end - - if !detached? - if attaching? - # Let the pending operation complete (#RTL5i) - once_state_changed { transition_state_machine :detaching if can_transition_to?(:detaching) } - elsif can_transition_to?(:detaching) - transition_state_machine :detaching - else - transition_state_machine! :detached - end - end - - deferrable_for_state_change_to(STATE.Detached, &success_block) - end - - # A {Ably::Realtime::Presence} object. - # - # @spec RTL9 - # - # @return {Ably::Realtime::Presence} - # - def presence - @presence - end - - # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical - # {Ably::Models::Message} objects for the channel. If the channel is configured to persist messages, - # then messages can be retrieved from history for up to 72 hours in the past. If not, messages can only - # be retrieved from history for up to two minutes in the past. - # - # @spec RSL2a - # - # @param (see {Ably::Rest::Channel#history}) - # @option options (see {Ably::Rest::Channel#history}) - # @option options [Boolean] :until_attach When true, the history request will be limited only to messages published before this channel was attached. Channel must be attached - # - # @yield [Ably::Models::PaginatedResult] First {Ably::Models::PaginatedResult page} of {Ably::Models::Message} objects accessible with {Ably::Models::PaginatedResult#items #items}. - # - # @return [Ably::Util::SafeDeferrable] - # - def history(options = {}, &callback) - # RTL10b - if options.delete(:until_attach) - unless attached? - error = Ably::Exceptions::InvalidRequest.new('option :until_attach is invalid as the channel is not attached' ) - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - options[:from_serial] = properties.attach_serial - end - - async_wrap(callback) do - rest_channel.history(options.merge(async_blocking_operations: true)) - end - end - - # @return [Ably::Util::PubSub] Client library internal channel incoming message bus - # - # @api private - def __incoming_msgbus__ - @__incoming_msgbus__ ||= Ably::Util::PubSub.new( - coerce_into: lambda { |event| Ably::Models::ProtocolMessage::ACTION(event) } - ) - end - - # Sets the {Ably::Models::ChannelOptions} for the channel. - # An optional callback may be provided to notify of the success or failure of the operation. - # - # @spec RTL16 - # - # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} - # @return [Ably::Models::ChannelOptions] - def set_options(channel_options) - @options = Ably::Models::ChannelOptions(channel_options) - # RTL4i - manager.request_reattach if (need_reattach? and connection.state?(:connected)) - end - alias options= set_options - - # @api private - def set_channel_error_reason(error) - @error_reason = error - end - - # @api private - def clear_error_reason - @error_reason = nil - end - - # Used by {Ably::Modules::StateEmitter} to debug state changes - # @api private - def logger - client.logger - end - - # @api private - def attach_resume! - @attach_resume = true - end - - # @api private - def reset_attach_resume! - @attach_resume = false - end - - # As we are using a state machine, do not allow change_state to be used - # #transition_state_machine must be used instead - private :change_state - - def need_reattach? - !!(attaching? || attached?) && !!(options.modes || options.params) - end - - private - - def setup_event_handlers - __incoming_msgbus__.subscribe(:message) do |message| - message.decode(client.encoders, options) do |encode_error, error_message| - client.logger.error error_message - end - emit_message message.name, message - end - end - - def rest_channel - client.rest_client.channel(name) - end - - def connection - client.connection - end - - def setup_presence - @presence ||= Presence.new(self) - end - - # Alias useful for methods with a name argument - def channel_name - name - end - end - end -end - -require 'ably/realtime/channel/channel_manager' -require 'ably/realtime/channel/channel_state_machine' -require 'ably/realtime/channel/push_channel' -require 'ably/realtime/channel/channel_properties' diff --git a/core/lib/ably/realtime/channels.rb b/core/lib/ably/realtime/channels.rb deleted file mode 100644 index e7cf788dd..000000000 --- a/core/lib/ably/realtime/channels.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Ably - module Realtime - # Class that maintains a map of Channels ensuring Channels are reused - class Channels - include Ably::Modules::ChannelsCollection - - # @return [Ably::Realtime::Channels] - # - def initialize(client) - super client, Ably::Realtime::Channel - end - - # Return a {Ably::Realtime::Channel} for the given name - # - # @param name [String] The name of the channel - # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} - # - # @return [Ably::Realtime::Channel] - # - def get(*args) - super - end - - # Return a {Ably::Realtime::Channel} for the given name if it exists, else the block will be called. - # This method is intentionally similar to {http://ruby-doc.org/core-2.1.3/Hash.html#method-i-fetch Hash#fetch} providing a simple way to check if a channel exists or not without creating one - # - # @param name [String] The name of the channel - # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called - # @yieldparam [String] name of the missing channel - # - # @return [Ably::Realtime::Channel] - # - def fetch(*args) - super - end - - # Detaches the {Ably::Realtime::Channel Realtime Channel} and releases all associated resources. - # - # Releasing a Realtime Channel is not typically necessary as a channel, once detached, consumes no resources other than - # the memory footprint of the {Ably::Realtime::Channel Realtime Channel object}. Release channels to free up resources if required - # - # @return [void] - # - def release(channel) - get(channel).detach do - @channels.delete(channel) - end if @channels.has_key?(channel) - end - - # Sets channel serial to each channel from given serials hashmap - # @param [Hash] serials - map of channel name to respective channel serial - # @api private - def set_channel_serials(serials) - serials.each do |channel_name, channel_serial| - get(channel_name).properties.channel_serial = channel_serial - end - end - - # @return [Hash] serials - map of channel name to respective channel serial - # @api private - def get_channel_serials - channel_serials = {} - self.each do |channel| - channel_serials[channel.name] = channel.properties.channel_serial if channel.state == :attached - end - channel_serials - end - - end - end -end diff --git a/core/lib/ably/realtime/client.rb b/core/lib/ably/realtime/client.rb deleted file mode 100644 index 57d943d02..000000000 --- a/core/lib/ably/realtime/client.rb +++ /dev/null @@ -1,377 +0,0 @@ -require 'uri' -require 'ably/realtime/channel/publisher' -require 'ably/realtime/recovery_key_context' - -module Ably - module Realtime - # A client that extends the functionality of the {Ably::Realtime::Client} and provides additional realtime-specific features. - # - class Client - include Ably::Modules::AsyncWrapper - include Ably::Realtime::Channel::Publisher - include Ably::Modules::Conversions - - extend Forwardable - using Ably::Util::AblyExtensions - - DOMAIN = 'realtime.ably.io' - - # A {Aby::Realtime::Channels} object. - # - # @spec RTC3, RTS1 - # - # @return [Aby::Realtime::Channels] - # - attr_reader :channels - - # An {Ably::Auth} object. - # - # @spec RTC4 - # - # @return [Ably::Auth] - # - attr_reader :auth - - # A {Aby::Realtime::Connection} object. - # - # @spec RTC2 - # - # @return [Aby::Realtime::Connection] - # - attr_reader :connection - - # The {Ably::Rest::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication - # @return [Ably::Rest::Client] - - # @private - attr_reader :rest_client - - # When false the client suppresses messages originating from this connection being echoed back on the same connection. Defaults to true - # @return [Boolean] - attr_reader :echo_messages - - # If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states. Defaults to true - # @return [Boolean] - attr_reader :queue_messages - - # The custom realtime websocket host that is being used if it was provided with the option `:ws_host` when the {Client} was created - # @return [String,Nil] - attr_reader :custom_realtime_host - - # When true, as soon as the client library is instantiated it will connect to Ably. If this attribute is false, a connection must be opened explicitly - # @return [Boolean] - attr_reader :auto_connect - - # When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery - # @return [String,Nil] - attr_reader :recover - - # Additional parameters to be sent in the querystring when initiating a realtime connection - # @return [Hash] - attr_reader :transport_params - - def_delegators :auth, :client_id, :auth_options - def_delegators :@rest_client, :encoders - def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary? - def_delegators :@rest_client, :environment, :custom_host, :custom_port, :custom_tls_port - def_delegators :@rest_client, :log_level - def_delegators :@rest_client, :options - - # Refuses direct construction: the package a client is created from is what declares - # the client's side to the platform, and a directly constructed client declares none. - # Use {Ably::PubSub::Server.create_realtime_client} from the +ably-pubsub-server+ gem. - # - # @raise [Ably::Exceptions::DirectConstructionNotSupported] always - def self.new(*args, **kwargs, &block) - raise Ably::Internal.direct_construction_error( - 'Ably::Realtime::Client.new', 'Ably::PubSub::Server.create_realtime_client(options)' - ) - end - - # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection. - # - # @spec RSC1 - # - # @param (see {Ably::Rest::Client#initialize}) - # @option options (see Ably::Rest::Client#initialize) An options {Hash} object. - # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required. - # Whilst the proc is called synchronously, it does not block the EventMachine reactor as it is run in a separate thread. - # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent - # @option options [Boolean] :queue_messages If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states - # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection - # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery - # @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect. - # @option options [Hash] :transport_params Additional parameters to be sent in the querystring when initiating a realtime connection. Keys are Strings, values are Stringifiable(a value must respond to #to_s) - # - # @option options [Integer] :channel_retry_timeout (15 seconds). When a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED - # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in seconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically - # @option options [Integer] :suspended_retry_timeout (30 seconds). When the connection enters the SUSPENDED state, after this delay in seconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically - # @option options [Boolean] :disable_websocket_heartbeats WebSocket heartbeats are more efficient than protocol level heartbeats, however they can be disabled for development purposes - # - # @return [Ably::Realtime::Client] - # - # @example - # # Constructs a client using an Ably API key or token string. - # client = Ably::PubSub::Server.create_realtime_client('key.id:secret') - # - # # Constructs a client using an Ably options object. - # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret', client_id: 'john') - # - # Applications do not call this constructor: {.new} refuses direct construction, and - # a per-side package reaches it through {Ably::Internal.create_realtime_client}. - # - def initialize(options) - raise ArgumentError, 'Options Hash is expected' if options.nil? - - options = options.clone - if options.kind_of?(String) - options = if options.match(Ably::Auth::API_KEY_REGEX) - { key: options } - else - { token: options } - end - end - - @transport_params = options.delete(:transport_params).to_h.each_with_object({}) do |(key, value), acc| - acc[key.to_s] = value.to_s - end - @rest_client = Ably::Internal.create_rest_client(options.merge(realtime_client: self)) - @echo_messages = rest_client.options.fetch_with_default(:echo_messages, true) - @queue_messages = rest_client.options.fetch_with_default(:queue_messages, true) - @custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host] - @auto_connect = rest_client.options.fetch_with_default(:auto_connect, true) - @recover = rest_client.options.fetch_with_default(:recover, '') - - @auth = Ably::Realtime::Auth.new(self) - @channels = Ably::Realtime::Channels.new(self) - @connection = Ably::Realtime::Connection.new(self, options) - - unless @recover.nil_or_empty? - recovery_context = RecoveryKeyContext.from_json(@recover, logger) - unless recovery_context.nil? - @channels.set_channel_serials recovery_context.channel_serials # RTN16j - @connection.set_msg_serial_from_recover = recovery_context.msg_serial # RTN16f - end - end - end - - # Return a {Ably::Realtime::Channel Realtime Channel} for the given name - # - # @param (see Ably::Realtime::Channels#get) - # @return (see Ably::Realtime::Channels#get) - # - def channel(name, channel_options = {}) - channels.get(name, channel_options) - end - - # Retrieves the time from the Ably service as milliseconds since the Unix epoch. Clients that do not have access - # to a sufficiently well maintained time source and wish to issue Ably {Ably::Models::TokenRequests} with - # a more accurate timestamp should use the queryTime property instead of this method. - # - # @spec RTC6a - # - # @yield [Time] The time as milliseconds since the Unix epoch. - # @return [Ably::Util::SafeDeferrable] - # - def time(&success_callback) - async_wrap(success_callback) do - rest_client.time - end - end - - # Queries the REST /stats API and retrieves your application's usage statistics. - # Returns a {Ably::Util::SafeDeferrable} object, containing an array of {Ably::Models::Stats} objects. See the Stats docs. - # - # @spec RTC5 - # - # @param (see Ably::Rest::Client#stats) - # @option options (see Ably::Rest::Client#stats) - # - # @yield [Ably::Models::PaginatedResult] A {Ably::Util::SafeDeferrable} object containing an array of {Ably::Models::Stats} objects. - # - # @return [Ably::Util::SafeDeferrable] - # - def stats(options = {}, &success_callback) - async_wrap(success_callback) do - rest_client.stats(options) - end - end - - # Calls {Connection#close} and causes the connection to close, entering the closing state. - # Once closed, the library will not attempt to re-establish the connection without an explicit call to {Connection#connect}. - # @spec RTN12 - # (see Ably::Realtime::Connection#close) - def close(&block) - connection.close(&block) - end - - # Calls {Ably::Realtime::Connection#connect} and causes the connection to open, entering the connecting - # state. Explicitly calling connect() is unnecessary unless the autoConnect property is disabled. - # @spec RTN11 - # (see Ably::Realtime::Connection#connect) - def connect(&block) - connection.connect(&block) - end - - # A {Ably::Realtime::Push} object. - # @return [Ably::Realtime::Push] - def push - @push ||= Push.new(self) - end - - # Makes a REST request to a provided path. This is provided as a convenience for developers who wish to use REST - # API functionality that is either not documented or is not yet included in the public API, without having to - # directly handle features such as authentication, paging, fallback hosts, MsgPack and JSON support. - # - # @spec RTC9 - # - # (see {Ably::Rest::Client#request}) - # @yield [Ably::Models::HttpPaginatedResponse<>] An Array of Stats - # - # @return [Ably::Util::SafeDeferrable] An {Ably::Util::SafeDeferrable} response object returned by the HTTP request, containing an empty or JSON-encodable object. - def request(method, path, params = {}, body = nil, headers = {}, &callback) - async_wrap(callback) do - rest_client.request(method, path, params, body, headers, async_blocking_operations: true) - end - end - - # Publish one or more messages to the specified channel. - # - # This method allows messages to be efficiently published to Ably without instancing a {Ably::Realtime::Channel} object. - # If you want to publish a high rate of messages to Ably without instancing channels or using the REST API, then this method - # is recommended. However, channel options such as encryption are not supported with this method. If you need to specify channel options - # we recommend you use the {Ably::Realtime::Channel} +publish+ method without attaching to each channel, unless you also want to subscribe - # to published messages on that channel. - # - # Note: This feature is still in beta. As such, we cannot guarantee the API will not change in future. - # - # @param channel [String] The channel name you want to publish the message(s) to - # @param name [String, Array, 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 - # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument - # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string - # - # @yield [Ably::Models::Message,Array] On success, will call the block with the {Ably::Models::Message} if a single message is published, or an Array of {Ably::Models::Message} when multiple messages are published - # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks - # - # @example - # # Publish a single message - # client.publish 'activityChannel', click', { x: 1, y: 2 } - # - # # Publish an array of message Hashes - # messages = [ - # { name: 'click', { x: 1, y: 2 } }, - # { name: 'click', { x: 2, y: 3 } } - # ] - # client.publish 'activityChannel', messages - # - # # Publish an array of Ably::Models::Message objects - # messages = [ - # Ably::Models::Message(name: 'click', { x: 1, y: 2 }) - # Ably::Models::Message(name: 'click', { x: 2, y: 3 }) - # ] - # client.publish 'activityChannel', messages - # - # client.publish('activityChannel', 'click', 'body') do |message| - # puts "#{message.name} event received with #{message.data}" - # end - # - # client.publish('activityChannel', 'click', 'body').errback do |error, message| - # puts "#{message.name} was not received, error #{error.message}" - # end - # - def publish(channel_name, name, data = nil, attributes = {}, &success_block) - if !connection.can_publish_messages? - error = Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. 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 - - messages = if name.kind_of?(Enumerable) - name - else - name = ensure_utf_8(:name, name, allow_nil: true) - ensure_supported_payload data - [{ name: name, data: data }.merge(attributes)] - end - - if messages.length > Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE - error = Ably::Exceptions::InvalidRequest.new("It is not possible to publish more than #{Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE} messages with a single publish request.") - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - enqueue_messages_on_connection(self, messages, channel_name).tap do |deferrable| - deferrable.callback(&success_block) if block_given? - end - end - - # @!attribute [r] endpoint - # @return [URI::Generic] Default Ably Realtime endpoint used for all requests - def endpoint - endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-')) - end - - # (see Ably::Rest::Client#register_encoder) - def register_encoder(encoder) - rest_client.register_encoder encoder - end - - # (see Ably::Rest::Client#fallback_hosts) - def fallback_hosts - rest_client.fallback_hosts - end - - # (see Ably::Rest::Client#logger) - def logger - @logger ||= Ably::Logger.new(self, log_level, rest_client.logger.custom_logger) - end - - # Disable connection recovery, typically used after a connection has been recovered - # @return [void] - # @api private - def disable_automatic_connection_recovery - @recover = nil - end - - # @!attribute [r] fallback_endpoint - # @return [URI::Generic] Fallback endpoint used to connect to the realtime Ably service. Note, after each connection attempt, a new random {Ably::FALLBACK_HOSTS fallback host} or provided fallback hosts are used - # @api private - def fallback_endpoint - unless defined?(@fallback_endpoints) && @fallback_endpoints - @fallback_endpoints = fallback_hosts.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) } - @fallback_endpoints << endpoint # Try the original host last if all fallbacks have been used - end - - fallback_endpoint_index = connection.manager.retry_count_for_state(:disconnected) + connection.manager.retry_count_for_state(:suspended) - 1 - - @fallback_endpoints[fallback_endpoint_index % @fallback_endpoints.count] - end - - # Retrieves a {Ably::Models::LocalDevice} object that represents the current state of the device as a target for push notifications. - # @spec RSH8 - # @return [Ably::Models::LocalDevice] A {Ably::Models::LocalDevice} object. - # - # @note This is unsupported in the Ruby library - def device - raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable' - end - - private - def endpoint_for_host(host) - port = if use_tls? - custom_tls_port - else - custom_port - end - - raise ArgumentError, "Custom port must be an Integer or nil" if port && !port.kind_of?(Integer) - - options = { - scheme: use_tls? ? 'wss' : 'ws', - host: host - } - options.merge!(port: port) if port - - URI::Generic.build(options) - end - end - end -end diff --git a/core/lib/ably/realtime/connection.rb b/core/lib/ably/realtime/connection.rb deleted file mode 100644 index 1f1affc98..000000000 --- a/core/lib/ably/realtime/connection.rb +++ /dev/null @@ -1,738 +0,0 @@ -require 'securerandom' - -module Ably - module Realtime - # Enables the management of a connection to Ably. - # - class Connection - include Ably::Modules::EventEmitter - include Ably::Modules::Conversions - include Ably::Modules::SafeYield - extend Ably::Modules::Enum - using Ably::Util::AblyExtensions - - - # The current {Ably::Realtime::Connection::STATE} of the connection. - # Describes the realtime [Connection]{@link Connection} object states. - # - # @spec RTN4d - # - # INITIALIZED A connection with this state has been initialized but no connection has yet been attempted. - # CONNECTING A connection attempt has been initiated. The connecting state is entered as soon as the library - # has completed initialization, and is reentered each time connection is re-attempted following disconnection. - # CONNECTED A connection exists and is active. - # DISCONNECTED A temporary failure condition. No current connection exists because there is no network connectivity - # or no host is available. The disconnected state is entered if an established connection is dropped, - # or if a connection attempt was unsuccessful. In the disconnected state the library will periodically - # attempt to open a new connection (approximately every 15 seconds), anticipating that the connection - # will be re-established soon and thus connection and channel continuity will be possible. - # In this state, developers can continue to publish messages as they are automatically placed - # in a local queue, to be sent as soon as a connection is reestablished. Messages published by - # other clients while this client is disconnected will be delivered to it upon reconnection, - # so long as the connection was resumed within 2 minutes. After 2 minutes have elapsed, recovery - # is no longer possible and the connection will move to the SUSPENDED state. - # SUSPENDED A long term failure condition. No current connection exists because there is no network connectivity - # or no host is available. The suspended state is entered after a failed connection attempt if - # there has then been no connection for a period of two minutes. In the suspended state, the library - # will periodically attempt to open a new connection every 30 seconds. Developers are unable to - # publish messages in this state. A new connection attempt can also be triggered by an explicit - # call to {Ably::Realtime::Connection#connect}. Once the connection has been re-established, - # channels will be automatically re-attached. The client has been disconnected for too long for them - # to resume from where they left off, so if it wants to catch up on messages published by other clients - # while it was disconnected, it needs to use the History API. - # CLOSING An explicit request by the developer to close the connection has been sent to the Ably service. - # If a reply is not received from Ably within a short period of time, the connection is forcibly - # terminated and the connection state becomes CLOSED. - # CLOSED The connection has been explicitly closed by the client. In the closed state, no reconnection attempts - # are made automatically by the library, and clients may not publish messages. No connection state is - # preserved by the service or by the library. A new connection attempt can be triggered by an explicit - # call to {Ably::Realtime::Connection#connect}, which results in a new connection. - # FAILED This state is entered if the client library encounters a failure condition that it cannot recover from. - # This may be a fatal connection error received from the Ably service, for example an attempt to connect - # with an incorrect API key, or a local terminal error, for example the token in use has expired - # and the library does not have any way to renew it. In the failed state, no reconnection attempts - # are made automatically by the library, and clients may not publish messages. A new connection attempt - # can be triggered by an explicit call to {Ably::Realtime::Connection#connect}. - # - # @return [Ably::Realtime::Connection::STATE] - # - STATE = ruby_enum('STATE', - :initialized, - :connecting, - :connected, - :disconnected, - :suspended, - :closing, - :closed, - :failed - ) - - # Describes the events emitted by a {Ably::Realtime::Connection} object. An event is either an UPDATE or a {Ably::Realtime::Connection::STATE}. - # - # UPDATE RTN4h An event for changes to connection conditions for which the {Ably::Realtime::Connection::STATE} does not change. - # - EVENT = ruby_enum('EVENT', - STATE.to_sym_arr + [:update] - ) - - include Ably::Modules::StateEmitter - include Ably::Modules::UsesStateMachine - ensure_state_machine_emits 'Ably::Models::ConnectionStateChange' - - # Defaults for automatic connection recovery and timeouts - DEFAULTS = { - channel_retry_timeout: 15, # when a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED - disconnected_retry_timeout: 15, # when the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically - suspended_retry_timeout: 30, # when the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically - connection_state_ttl: 120, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected - max_connection_state_ttl: nil, # allow a max TTL to be passed in, usually for CI test purposes thus overiding any connection_state_ttl sent from Ably - realtime_request_timeout: 10, # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage - websocket_heartbeats_disabled: false, - }.freeze - - # Max number of messages to bundle in a single ProtocolMessage - MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50 - - # A unique public identifier for this connection, used to identify this member. - # - # @spec RTN8 - # - # @return [String] - # - attr_reader :id - - # A unique private connection key used to recover or resume a connection, assigned by Ably. - # When recovering a connection explicitly, the recoveryKey is used in the recover client options as it contains - # both the key and the last message serial. This private connection key can also be used by other REST clients - # to publish on behalf of this client. See the publishing over REST on behalf of a realtime client docs for more info. - # - # @spec RTN9 - # - # @return [String] - # - attr_reader :key - - # An {Ably::Models::ErrorInfo} object describing the last error received if a connection failure occurs. - # - # @spec RTN14a - # - # @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException] - # - attr_reader :error_reason - - # Connection details of the currently established connection - # @return [Ably::Models::ConnectionDetails] - attr_reader :details - - # {Ably::Realtime::Client} associated with this connection - # @return [Ably::Realtime::Client] - attr_reader :client - - # Underlying socket transport used for this connection, for internal use by the client library - # @return [Ably::Realtime::Connection::WebsocketTransport] - # @api private - attr_reader :transport - - # The Connection manager responsible for creating, maintaining and closing the connection and underlying transport - # @return [Ably::Realtime::Connection::ConnectionManager] - # @api private - attr_reader :manager - - # An internal queue used to manage unsent outgoing messages. You should never interface with this array directly - # @return [Array] - # @api private - attr_reader :__outgoing_message_queue__ - - # An internal queue used to manage sent messages. You should never interface with this array directly - # @return [Array] - # @api private - attr_reader :__pending_message_ack_queue__ - - # Configured recovery and timeout defaults for this {Connection}. - # See the configurable options in {Ably::Realtime::Client#initialize}. - # The defaults are immutable - # @return [Hash] - attr_reader :defaults - - # @api public - def initialize(client, options) - @client = client - @__outgoing_message_queue__ = [] - @__pending_message_ack_queue__ = [] - - @defaults = DEFAULTS.dup - options.each do |key, val| - @defaults[key] = val if DEFAULTS.has_key?(key) - end if options.kind_of?(Hash) - @defaults.freeze - - Client::IncomingMessageDispatcher.new client, self - Client::OutgoingMessageDispatcher.new client, self - - @state_machine = ConnectionStateMachine.new(self) - @state = STATE(state_machine.current_state) - @manager = ConnectionManager.new(self) - - @current_host = client.endpoint.host - - reset_client_msg_serial - end - - # Causes the connection to close, entering the {Ably::Realtime::Connection::STATE} CLOSING state. - # Once closed, the library does not attempt to re-establish the connection without an explicit call to - # {Ably::Realtime::Connection#connect}. - # - # @spec RTN12 - # - # @yield block is called as soon as this connection is in the Closed state - # - # @return [EventMachine::Deferrable] - # - def close(&success_block) - unless closing? || closed? - unless can_transition_to?(:closing) - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:closing)) - end - transition_state_machine :closing - end - deferrable_for_state_change_to(STATE.Closed, &success_block) - end - - # Explicitly calling connect() is unnecessary unless the autoConnect attribute of - # the ClientOptions object is false. Unless already connected or connecting, - # this method causes the connection to open, entering the {Ably::Realtime::Connection::STATE} CONNECTING state. - # - # @spec RTC1b, RTN3, RTN11 - # - # @yield block is called as soon as this connection is in the Connected state - # - # @return [EventMachine::Deferrable] - # - def connect(&success_block) - unless connecting? || connected? - unless can_transition_to?(:connecting) - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:connecting)) - end - # If connect called in a suspended block, we want to ensure the other callbacks have finished their work first - EventMachine.next_tick { transition_state_machine :connecting if can_transition_to?(:connecting) } - end - - Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| - deferrable.callback do - yield if block_given? - end - succeed_callback = deferrable.method(:succeed) - fail_callback = deferrable.method(:fail) - - unsafe_once(:connected) do - deferrable.succeed - off(&fail_callback) - end - - unsafe_once(:failed, :closed, :closing) do - deferrable.fail - off(&succeed_callback) - end - end - end - - # When connected, sends a heartbeat ping to the Ably server and executes the callback with any error - # and the response time in milliseconds when a heartbeat ping request is echoed from the server. - # This can be useful for measuring true round-trip latency to the connected Ably server. - # - # @spec RTN13 - # - # @yield [Integer] if a block is passed to this method, then this block will be called once the ping heartbeat is received with the time elapsed in seconds. - # If the ping is not received within an acceptable timeframe, the block will be called with +nil+ as he first argument - # - # @example - # client = Ably::PubSub::Server.create_realtime_client(key: 'key.id:secret') - # client.connection.ping do |elapsed_s| - # puts "Ping took #{elapsed_s}s" - # end - # - # @return [Ably::Util::SafeDeferrable] - # - def ping(&block) - if initialized? || suspended? || closing? || closed? || failed? - error = Ably::Models::ErrorInfo.new(message: "Cannot send a ping when the connection is #{state}", code: Ably::Exceptions::Codes::DISCONNECTED) - return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error) - end - - Ably::Util::SafeDeferrable.new(logger).tap do |deferrable| - started = nil - finished = false - ping_id = SecureRandom.hex(16) - heartbeat_action = Ably::Models::ProtocolMessage::ACTION.Heartbeat - - wait_for_ping = lambda do |protocol_message| - next if finished - if protocol_message.action == heartbeat_action && protocol_message.id == ping_id - finished = true - __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping) - time_passed = Time.now.to_f - started.to_f - deferrable.succeed time_passed - safe_yield block, time_passed if block_given? - end - end - - once_or_if(STATE.Connected) do - next if finished - started = Time.now - send_protocol_message action: heartbeat_action.to_i, id: ping_id - __incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping - end - - once_or_if([:suspended, :closing, :closed, :failed]) do - next if finished - finished = true - deferrable.fail Ably::Models::ErrorInfo.new(message: "Ping failed as connection has changed state to #{state}", code: Ably::Exceptions::Codes::DISCONNECTED) - end - - EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do - next if finished - finished = true - __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping) - error_msg = "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s" - logger.warn { error_msg } - deferrable.fail Ably::Models::ErrorInfo.new(message: error_msg, code: Ably::Exceptions::Codes::TIMEOUT_ERROR) - safe_yield block, nil if block_given? - end - end - end - - # @yield [Boolean] True if an internet connection check appears to be up following an HTTP request to a reliable CDN - # @return [EventMachine::Deferrable] - # @api private - def internet_up? - url = "http#{'s' if client.use_tls?}:#{Ably::INTERNET_CHECK.fetch(:url)}" - EventMachine::DefaultDeferrable.new.tap do |deferrable| - EventMachine::AblyHttpRequest::HttpRequest.new(url, tls: { verify_peer: true }).get.tap do |http| - http.errback do - yield false if block_given? - deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED) - end - http.callback do - EventMachine.next_tick do - result = http.response_header.status == 200 && http.response.strip == Ably::INTERNET_CHECK.fetch(:ok_text) - yield result if block_given? - if result - deferrable.succeed - else - deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unexpected response from #{url} (#{http.response_header.status})", 400, Ably::Exceptions::Codes::BAD_REQUEST) - end - end - end - end - end - end - - # The recovery key string can be used by another client to recover this connection's state in the - # recover client options property. See connection state recover options for more information. - # - # @spec RTN16b, RTN16c - # - # @deprecated Use {#create_recovery_key} instead - # - def recovery_key - logger.warn "[DEPRECATION] recovery_key is deprecated, use create_recovery_key method instead" - create_recovery_key - end - - # The recovery key string can be used by another client to recover this connection's state in the recover client - # options property. See connection state recover options for more information. - # - # @spec RTN16g, RTN16c - # - # @return [String] a json string which incorporates the @connectionKey@, the current @msgSerial@ and collection - # of pairs of channel @name@ and current @channelSerial@ for every currently attached channel - def create_recovery_key - if key.nil_or_empty? || state == :closing || state == :closed || state == :failed || state == :suspended - return nil #RTN16g2 - end - RecoveryKeyContext.new(key, client_msg_serial, client.channels.get_channel_serials).to_json - end - - # Following a new connection being made, the connection ID, connection key - # need to match the details provided by the server. - # - # @return [void] - # @api private - def configure_new(connection_id, connection_key) - @id = connection_id - @key = connection_key - end - - # Disable automatic resume of a connection - # @return [void] - # @api private - def reset_resume_info - @key = nil - @id = nil - end - - # @!attribute [r] __outgoing_protocol_msgbus__ - # @return [Ably::Util::PubSub] Client library internal outgoing protocol message bus - # @api private - def __outgoing_protocol_msgbus__ - @__outgoing_protocol_msgbus__ ||= create_pub_sub_message_bus - end - - # @!attribute [r] __incoming_protocol_msgbus__ - # @return [Ably::Util::PubSub] Client library internal incoming protocol message bus - # @api private - def __incoming_protocol_msgbus__ - @__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus - end - - # Determines the correct host name to use for the next connection attempt and updates current_host - # @yield [String] The host name used for this connection, for network connection failures a {Ably::FALLBACK_HOSTS fallback host} is used to route around networking or intermittent problems if an Internet connection is available - # @api private - def determine_host - raise ArgumentError, 'Block required' unless block_given? - - if should_use_fallback_hosts? - internet_up? do |internet_is_up_result| - @current_host = if internet_is_up_result - client.fallback_endpoint.host - else - client.endpoint.host - end - yield current_host - end - else - @current_host = client.endpoint.host - yield current_host - end - end - - # @return [String] The current host that is configured following a call to method {#determine_host} - # @api private - attr_reader :current_host - - # @!attribute [r] port - # @return [Integer] The default port used for this connection - def port - client.use_tls? ? client.custom_tls_port || 443 : client.custom_port || 80 - end - - # @!attribute [r] logger - # @return [Logger] The {Ably::Logger} for this client. - # Configure the log_level with the `:log_level` option, refer to {Ably::Realtime::Client#initialize} - def logger - client.logger - end - - # Add protocol message to the outgoing message queue and notify the dispatcher that a message is - # ready to be sent - # - # @param [Ably::Models::ProtocolMessage] protocol_message - # @return [void] - # @api private - def send_protocol_message(protocol_message) - add_message_serial_if_ack_required_to(protocol_message) do - message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger) - add_message_to_outgoing_queue(message) - notify_message_dispatcher_of_new_message message - end - end - - def send_protocol_message_immediately(protocol_message) - message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger) - add_message_to_outgoing_queue(message, true) - notify_message_dispatcher_of_new_message message - end - - # @api private - def add_message_to_outgoing_queue(protocol_message, send_immediately = false) - if send_immediately - # Adding msg at the top of the queue to get processed immediately while connection is CONNECTED - __outgoing_message_queue__.prepend(protocol_message) - logger.debug { "Connection: protocol msg pushed at the top =>: #{protocol_message.action} #{protocol_message}" } - else - __outgoing_message_queue__ << protocol_message - logger.debug { "Connection: protocol msg queued =>: #{protocol_message.action} #{protocol_message}" } - end - end - - # @api private - def notify_message_dispatcher_of_new_message(protocol_message) - __outgoing_protocol_msgbus__.publish :protocol_message, protocol_message - end - - # @return [EventMachine::Deferrable] - # @api private - def create_websocket_transport - EventMachine::DefaultDeferrable.new.tap do |websocket_deferrable| - # Getting auth params can be blocking so uses a Deferrable - client.auth.auth_params.tap do |auth_deferrable| - auth_deferrable.callback do |auth_params| - url_params = auth_params.merge( - 'format' => client.protocol, - 'echo' => client.echo_messages, - 'v' => Ably::PROTOCOL_VERSION, # RSC7a - 'agent' => client.rest_client.agent - ) - - # Use native websocket heartbeats if possible, but allow Ably protocol heartbeats - url_params['heartbeats'] = if defaults.fetch(:websocket_heartbeats_disabled) - 'true' - else - 'false' - end - # RSA7e1 - url_params['clientId'] = client.auth.client_id_for_request_sync if client.auth.client_id_for_request_sync - url_params.merge!(client.transport_params) - - if !key.nil_or_empty? and connection_state_available? - url_params.merge! resume: key - logger.debug { "Resuming connection with key #{key}" } - elsif !client.recover.nil_or_empty? - recovery_context = RecoveryKeyContext.from_json(client.recover, logger) - unless recovery_context.nil? - key = recovery_context.connection_key - logger.debug { "Recovering connection with key #{key}" } - url_params.merge! recover: key - end - end - - url = URI(client.endpoint).tap do |endpoint| - endpoint.query = URI.encode_www_form(url_params) - end - - determine_host do |host| - # Ensure the hostname matches the fallback host name - url.hostname = host - url.port = port - - begin - logger.debug { "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}" } - @transport = create_transport(host, port, url) do |websocket_transport| - websocket_deferrable.succeed websocket_transport - end - rescue EventMachine::ConnectionError => error - websocket_deferrable.fail error - end - end - end - - auth_deferrable.errback do |error| - websocket_deferrable.fail error - end - end - end - end - - # @api private - def release_websocket_transport - @transport = nil - end - - # @api private - def set_failed_connection_error_reason(error) - @error_reason = error - end - - # @api private - def clear_error_reason - @error_reason = nil - end - - # @api private - def set_connection_details(connection_details) - @details = connection_details - end - - # Returns false if messages cannot be published as a result of message queueing being disabled - # @api private - def can_publish_messages? - connected? || - ( (initialized? || connecting? || disconnected?) && client.queue_messages ) - end - - # @api private - def create_transport(host, port, url, &block) - logger.debug { "Connection: EventMachine connecting to #{host}:#{port} with URL: #{url}" } - EventMachine.connect(host, port, WebsocketTransport, self, url.to_s, &block) - end - - # @api private - def connection_state_ttl - defaults[:max_connection_state_ttl] || # undocumented max TTL configuration - (details && details.connection_state_ttl) || - defaults.fetch(:connection_state_ttl) - end - - def connection_state_ttl=(val) - @connection_state_ttl = val - end - - # @api private - def heartbeat_interval - # See RTN23a - (details && details.max_idle_interval).to_i + - defaults.fetch(:realtime_request_timeout) - end - - # Resets the client message serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage} - # (see #client_msg_serial) - # @api private - def reset_client_msg_serial - @client_msg_serial = -1 - end - - # Sets the client message serial from recover clientOption. - # @api private - def set_msg_serial_from_recover=(value) - @client_msg_serial = value - end - - # When a hearbeat or any other message from Ably is received - # we know it's alive, see #RTN23 - # @api private - def set_connection_confirmed_alive - @last_liveness_event = Time.now - manager.reset_liveness_timer - end - - # @api private - def time_since_connection_confirmed_alive? - Time.now.to_i - @last_liveness_event.to_i - end - - # As we are using a state machine, do not allow change_state to be used - # #transition_state_machine must be used instead - private :change_state - - private - - # The client message serial (msgSerial) is incremented for every message that is published that requires an ACK. - # A message serial number does not guarantee a message has been received, only sent. - # @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent - def client_msg_serial - @client_msg_serial - end - - def create_pub_sub_message_bus - Ably::Util::PubSub.new( - coerce_into: lambda do |event| - raise KeyError, "Expected :protocol_message, :#{event} is disallowed" unless event == :protocol_message - :protocol_message - end - ) - end - - def add_message_serial_if_ack_required_to(protocol_message) - if Ably::Models::ProtocolMessage.ack_required?(protocol_message[:action]) - add_message_serial_to(protocol_message) { yield } - else - yield - end - end - - def add_message_serial_to(protocol_message) - @client_msg_serial += 1 - protocol_message[:msgSerial] = client_msg_serial - yield - rescue StandardError => e - @client_msg_serial -= 1 - raise e - end - - # Simply wait until the next EventMachine tick to ensure Connection initialization is complete - def when_initialized - EventMachine.next_tick { yield } - end - - def connection_state_available? - return true if connected? - - return false if time_since_connection_confirmed_alive? > connection_state_ttl + details.max_idle_interval - - connected_last = state_history.reverse.find { |connected| connected.fetch(:state) == :connected } - if connected_last.nil? - false - else - true - end - end - - def production? - client.environment.nil? || client.environment == :production - end - - def custom_port? - if client.use_tls? - !!client.custom_tls_port - else - !!client.custom_port - end - end - - def custom_host? - !!client.custom_realtime_host - end - - def should_use_fallback_hosts? - if client.fallback_hosts && !client.fallback_hosts.empty? - if connecting? && previous_state && !disconnected_from_connected_state? - use_fallback_if_disconnected? || use_fallback_if_suspended? - end - end - end - - def disconnected_from_connected_state? - most_recent_state_changes = state_history.last(3).first(2) # Ignore current state - - # A valid connection was disconnected - most_recent_state_changes.last.fetch(:state) == Connection::STATE.Disconnected && - most_recent_state_changes.first.fetch(:state) == Connection::STATE.Connected - end - - def use_fallback_if_disconnected? - unless second_reconnect_attempt_for(:disconnected, 1) - return false - end - - does_error_necessitate_fallback(reason_for_last_time_in(:disconnected)) - end - - def use_fallback_if_suspended? - unless second_reconnect_attempt_for(:suspended, 2) # on first suspended state use default Ably host again - return false - end - - does_error_necessitate_fallback(reason_for_last_time_in(:suspended)) - end - - def second_reconnect_attempt_for(state, first_attempt_count) - previous_state == state && manager.retry_count_for_state(state) >= first_attempt_count - end - - # Provides a partial implementation of RTN17f's logic for whether an error necessitates a fallback host. - def does_error_necessitate_fallback(error) - return false unless error - - # For now we just explicitly exclude token errors. TODO: implement properly in https://github.com/ably/ably-ruby/issues/444 - - if error.respond_to?(:status_code) && error.status_code == 401 && error.respond_to?(:code) && Ably::Exceptions::TOKEN_EXPIRED_CODE.include?(error.code) - return false - end - - true - end - - # Returns the error associated with the last state change to the given state (e.g. :disconnected). - def reason_for_last_time_in(state) - history_item = state_history.reverse.find do |history_item| - history_item.fetch(:state) == state - end.fetch(:metadata).reason - end - end - end -end - -require 'ably/realtime/connection/connection_manager' -require 'ably/realtime/connection/connection_state_machine' -require 'ably/realtime/connection/websocket_transport' -require 'ably/realtime/recovery_key_context' diff --git a/core/lib/ably/realtime/push.rb b/core/lib/ably/realtime/push.rb deleted file mode 100644 index 04d7e0381..000000000 --- a/core/lib/ably/realtime/push.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'ably/realtime/push/admin' - -module Ably - module Realtime - # Class providing push notification functionality - class Push - # @private - attr_reader :client - - def initialize(client) - @client = client - end - - # A {Ably::Realtime::Push::Admin} object. - # - # @spec RSH1 - # - # @return [Ably::Realtime::Push::Admin] - # - def admin - @admin ||= Admin.new(self) - end - end - end -end diff --git a/core/lib/ably/realtime/recovery_key_context.rb b/core/lib/ably/realtime/recovery_key_context.rb deleted file mode 100644 index 30ec3bc2e..000000000 --- a/core/lib/ably/realtime/recovery_key_context.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'json' -# frozen_string_literal: true - -module Ably - module Realtime - class RecoveryKeyContext - attr_reader :connection_key - attr_reader :msg_serial - attr_reader :channel_serials - - def initialize(connection_key, msg_serial, channel_serials) - @connection_key = connection_key - @msg_serial = msg_serial - @channel_serials = channel_serials - if @channel_serials.nil? - @channel_serials = {} - end - end - - def to_json - { 'connection_key' => @connection_key, 'msg_serial' => @msg_serial, 'channel_serials' => @channel_serials }.to_json - end - - def self.from_json(obj, logger = nil) - begin - data = JSON.load obj - self.new data['connection_key'], data['msg_serial'], data['channel_serials'] - rescue => e - logger.warn "unable to decode recovery key, found error #{e}" unless logger.nil? - return nil - end - end - - end - end -end diff --git a/core/lib/ably/rest.rb b/core/lib/ably/rest.rb deleted file mode 100644 index eca5d41d4..000000000 --- a/core/lib/ably/rest.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'ably/rest/channel' -require 'ably/rest/channels' -require 'ably/rest/client' -require 'ably/rest/push' -require 'ably/rest/presence' - -require 'ably/models/message_encoders/base' - -Dir.glob(File.expand_path("models/*.rb", File.dirname(__FILE__))).each do |file| - require file -end - -module Ably - # Rest is the namespace of the stateless HTTP client and the models it returns. - # - # @example - # client = Ably::PubSub::Server.create_http_client("xxxxx") - # channel = client.channel("test") - # channel.publish "greeting", "data" - # - module Rest - # Refuses construction. This was a convenience alias for the {Ably::Rest::Client} - # constructor, which no longer accepts direct construction: the package a client is - # created from is what declares the client's side to the platform, and a client - # constructed here declares none. - # - # Use {Ably::PubSub::Server.create_http_client} from the +ably-pubsub-server+ gem. - # - # @raise [Ably::Exceptions::DirectConstructionNotSupported] always - def self.new(*args, **kwargs, &block) - raise Ably::Internal.direct_construction_error( - 'Ably::Rest.new', 'Ably::PubSub::Server.create_http_client(options)' - ) - end - end -end diff --git a/core/lib/ably/rest/channel.rb b/core/lib/ably/rest/channel.rb deleted file mode 100644 index a8bcfb83a..000000000 --- a/core/lib/ably/rest/channel.rb +++ /dev/null @@ -1,194 +0,0 @@ -module Ably - module Rest - # Enables messages to be published and historic messages to be retrieved for a channel. - # - class Channel - include Ably::Modules::Conversions - - # Ably client associated with this channel - # @return [Ably::Realtime::Client] - # @api private - attr_reader :client - - # The channel name. - # @return [String] - attr_reader :name - - attr_reader :options - - # A {Ably::Rest::Channel::PushChannel} object - # @spec RSH4 - # @return [Ably::Rest::Channel::PushChannel] - # @api private - attr_reader :push - - IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH = 9 # See spec RSL1k1 - - # Initialize a new Channel object - # - # @param client [Ably::Rest::Client] - # @param name [String] The name of the channel - # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} - # - def initialize(client, name, channel_options = {}) - name = (ensure_utf_8 :name, name) - - @options = Ably::Models::ChannelOptions(channel_options) - @client = client - @name = name - @push = PushChannel.new(self) - end - - # Publishes a message to the channel. A callback may optionally be passed in to this call to be notified of success or failure of the operation. - # - # @spec RSL1 - # - # @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 - # - # @example - # # Publish a single message with (name, data) form - # channel.publish 'click', { x: 1, y: 2 } - # - # # Publish a single message with single Hash form - # message = { name: 'click', data: { x: 1, y: 2 } } - # channel.publish message - # - # # Publish an array of message Hashes form - # messages = [ - # { name: 'click', data: { x: 1, y: 2 } }, - # { name: 'click', data: { x: 2, y: 3 } } - # ] - # channel.publish messages - # - # # Publish an array of Ably::Models::Message objects form - # messages = [ - # Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) - # Ably::Models::Message(name: 'click', data: { x: 2, y: 3 }) - # ] - # channel.publish messages - # - # # Publish a single Ably::Models::Message object form - # message = Ably::Models::Message(name: 'click', data: { x: 1, y: 2 }) - # channel.publish message - # - def publish(name, data = nil, attributes = {}) - qs_params = nil - qs_params = data if name.kind_of?(Enumerable) || name.kind_of?(Ably::Models::Message) - - messages = build_messages(name, data, attributes) # (RSL1a, RSL1b) - - if messages.sum(&:size) > (max_message_size = client.max_message_size || Ably::Rest::Client::MAX_MESSAGE_SIZE) - raise Ably::Exceptions::MaxMessageSizeExceeded.new("Maximum message size exceeded #{max_message_size} bytes.") - end - - payload = messages.map do |message| - Ably::Models::Message(message.dup).tap do |msg| - msg.encode client.encoders, options - - next if msg.client_id.nil? - if msg.client_id == '*' - raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages') - end - unless client.auth.can_assume_client_id?(msg.client_id) - raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{msg.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'") - end - end.as_json - end.tap do |payload| - if client.idempotent_rest_publishing - # We cannot mutate for idempotent publishing if one or more messages already has an ID - if payload.all? { |msg| !msg['id'] } - # Mutate the JSON to support idempotent publishing where a Message.id does not exist - idempotent_publish_id = SecureRandom.base64(IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH) - payload.each_with_index do |msg, idx| - msg['id'] = "#{idempotent_publish_id}:#{idx}" - end - end - end - end - - 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) - end - - # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical {Ably::Models::Message} - # objects for the channel. If the channel is configured to persist messages, then messages can be retrieved from - # history for up to 72 hours in the past. If not, messages can only be retrieved from history for up to two minutes in the past. - # - # @spec RSL2a - # - # @param [Hash] options the options for the message history request - # @option options [Integer,Time] :start The time from which messages are retrieved, specified as milliseconds since the Unix epoch. RSL2b1 - # @option options [Integer,Time] :end The time until messages are retrieved, specified as milliseconds since the Unix epoch. RSL2b1 - # @option options [Symbol] :direction The order for which messages are returned in. Valid values are backwards which orders messages from most recent to oldest, or forwards which orders messages from oldest to most recent. The default is backwards. RSL2b2 - # @option options [Integer] :limit An upper limit on the number of messages returned. The default is 100, and the maximum is 1000. RSL2b3 - # - # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::Message} objects. - # - def history(options = {}) - url = "#{base_path}/messages" - options = { - :direction => :backwards, - :limit => 100 - }.merge(options) - - [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) } - raise ArgumentError, ":end must be equal to or after :start" if options[:start] && options[:end] && (options[:start] > options[:end]) - - paginated_options = { - coerce_into: 'Ably::Models::Message', - async_blocking_operations: options.delete(:async_blocking_operations), - } - - response = client.get(url, options) - - Ably::Models::PaginatedResult.new(response, url, client, paginated_options) do |message| - message.tap do |msg| - decode_message msg - end - end - end - - # A {Ably::Rest::Presence} object. - # @spec RSL3 - # @return [Ably::Rest::Presence] - def presence - @presence ||= Presence.new(client, self) - end - - # Sets the {Ably::Models::ChannelOptions} for the channel. - # @spec RSL7 - # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} - # @return [Ably::Models::ChannelOptions] - def set_options(channel_options) - @options = Ably::Models::ChannelOptions(channel_options) - end - alias options= set_options - - # Retrieves a {Ably::Models::ChannelDetails} object for the channel, which includes status and occupancy metrics. - # @spec RSL8 - # @return [Ably::Models::ChannelDetails] A {Ably::Models::ChannelDetails} object. - def status - Ably::Models::ChannelDetails.new(client.get(base_path).body) - end - - private - - def base_path - "/channels/#{URI.encode_www_form_component(name)}" - end - - def decode_message(message) - message.decode client.encoders, options - rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e - client.logger.error { "Decoding Error on channel '#{name}', message event name '#{message.name}'. #{e.class.name}: #{e.message}" } - end - end - end -end - -require 'ably/rest/channel/push_channel' diff --git a/core/lib/ably/rest/channels.rb b/core/lib/ably/rest/channels.rb deleted file mode 100644 index 6bfdbea10..000000000 --- a/core/lib/ably/rest/channels.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Ably - module Rest - class Channels - include Ably::Modules::ChannelsCollection - - # @return [Ably::Rest::Channels] - def initialize(client) - super client, Ably::Rest::Channel - end - - # Return a {Ably::Rest::Channel} for the given name - # - # @param name [String] The name of the channel - # @param channel_options [Hash] Channel options, currently reserved for Encryption options - # - # @return [Ably::Rest::Channel] - # - def get(*args) - super - end - - # Return a {Ably::Rest::Channel} for the given name if it exists, else the block will be called. - # This method is intentionally similar to {http://ruby-doc.org/core-2.1.3/Hash.html#method-i-fetch Hash#fetch} providing a simple way to check if a channel exists or not without creating one - # - # @param name [String] The name of the channel - # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called - # @yieldparam [String] name of the missing channel - # - # @return [Ably::Rest::Channel] - # - def fetch(*args) - super - end - - # Destroy the {Ably::Rest::Channel} and releases the associated resources. - # - # Releasing a {Ably::Rest::Channel} is not typically necessary as a channel consumes no resources other than the memory footprint of the - # {Ably::Rest::Channel} object. Explicitly release channels to free up resources if required - # - # @return [void] - # - def release(*args) - super - end - end - end -end diff --git a/core/lib/ably/rest/client.rb b/core/lib/ably/rest/client.rb deleted file mode 100644 index b7404d00d..000000000 --- a/core/lib/ably/rest/client.rb +++ /dev/null @@ -1,741 +0,0 @@ -require 'faraday' -require 'json' -require 'logger' -require 'uri' - -require 'typhoeus' -require 'faraday/typhoeus' - -require 'ably/rest/middleware/exceptions' - -module Ably - module Rest - # A client that offers a simple stateless API to interact directly with Ably's REST API. - # - class Client - include Ably::Modules::Conversions - include Ably::Modules::HttpHelpers - extend Forwardable - using Ably::Util::AblyExtensions - - # Default Ably domain for REST - DOMAIN = 'rest.ably.io' - - MAX_MESSAGE_SIZE = 65536 # See spec TO3l8 - MAX_FRAME_SIZE = 524288 # See spec TO3l8 - - # Configuration for HTTP timeouts and HTTP request reattempts to fallback hosts - HTTP_DEFAULTS = { - open_timeout: 4, - request_timeout: 10, - max_retry_duration: 15, - max_retry_count: 3 - }.freeze - - FALLBACK_RETRY_TIMEOUT = 10 * 60 - - # Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError - FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError) - [Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError] - else - Faraday::ClientError - end - - def_delegators :auth, :client_id, :auth_options - - # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment - # @return [String] - attr_reader :environment - - # The protocol configured for this client, either binary `:msgpack` or text based `:json` - # @return [Symbol] - attr_reader :protocol - - # Client agent i.e. `example-gem/1.2.0 ably-pubsub-ruby/1.1.5 ruby/3.1.1` - # @return [String] - attr_reader :agent - - # An {Ably::Auth} object. - # @spec RSC5 - # @return [Ably::Auth] - attr_reader :auth - - # A {Ably::Rest::Channels} object. - # @spec RSN1 - # @return [Aby::Rest::Channels] - attr_reader :channels - - # Log level configured for this {Client} - # @return [Logger::Severity] - attr_reader :log_level - - # The custom host that is being used if it was provided with the option +:rest_host+ when the {Client} was created - # @return [String,Nil] - attr_reader :custom_host - - # The custom port for non-TLS requests if it was provided with the option +:port+ when the {Client} was created - # @return [Integer,Nil] - attr_reader :custom_port - - # The custom TLS port for TLS requests if it was provided with the option +:tls_port+ when the {Client} was created - # @return [Integer,Nil] - attr_reader :custom_tls_port - - # The immutable configured HTTP defaults for this client. - # See {#initialize} for the configurable HTTP defaults prefixed with +http_+ - # @return [Hash] - attr_reader :http_defaults - - # The registered encoders that are used to encode and decode message payloads - # @return [Array] - # @api private - attr_reader :encoders - - # The additional options passed to this Client's #initialize method not available as attributes of this class - # @return [Hash] - # @api private - attr_reader :options - - # The list of fallback hosts to be used by this client - # if empty or nil then fallback host functionality is disabled - attr_reader :fallback_hosts - - # Whether the {Client} has to add a random identifier to the path of a request - # @return [Boolean] - attr_reader :add_request_ids - - # Retries are logged by default to warn and error. When true, retries are logged at info level - # @return [Boolean] - # @api private - attr_reader :log_retries_as_info - - # True when idempotent publishing is enabled for all messages published via REST. - # When this feature is enabled, the client library will add a unique ID to every published message (without an ID) - # ensuring any failed published attempts (due to failures such as HTTP requests failing mid-flight) that are - # automatically retried will not result in duplicate messages being published to the Ably platform. - # Note: This is a beta unsupported feature! - # @return [Boolean] - attr_reader :idempotent_rest_publishing - - # Max message size (TO2, TO3l8) by default (65536 bytes) 64KiB - # @return [Integer] - attr_reader :max_message_size - - # Max frame size (TO2, TO3l8) by default (524288 bytes) 512KiB - # @return [Integer] - attr_reader :max_frame_size - - # Refuses direct construction: the package a client is created from is what declares - # the client's side to the platform, and a directly constructed client declares none. - # Use {Ably::PubSub::Server.create_http_client} from the +ably-pubsub-server+ gem. - # - # @raise [Ably::Exceptions::DirectConstructionNotSupported] always - def self.new(*args, **kwargs, &block) - raise Ably::Internal.direct_construction_error( - 'Ably::Rest::Client.new', 'Ably::PubSub::Server.create_http_client(options)' - ) - end - - # Constructs a {Ably::Rest::Client} object using an Ably API key or token string. - # - # @spec RSC1 - # - # @param [Hash,String] options an options Hash or String used to configure the client and the authentication, or String with an API key or Token ID - # @option options [Boolean] :tls (true) When false, TLS is disabled. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections. - # @option options [String] :key API key comprising the key name and key secret in a single string - # @option options [String] :token Token string or {Models::TokenDetails} used to authenticate requests - # @option options [String] :token_details {Models::TokenDetails} used to authenticate requests - # @option options [Boolean] :use_token_auth Will force Basic Auth if set to false, and Token auth if set to true - # @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment - # @option options [Symbol] :protocol (:msgpack) Protocol used to communicate with Ably, :json and :msgpack currently supported - # @option options [Boolean] :use_binary_protocol (true) When true will use the MessagePack binary protocol, when false it will use JSON encoding. This option will overide :protocol option - # @option options [Logger::Severity,Symbol] :log_level (Logger::WARN) Log level for the standard Logger that outputs to STDOUT. Can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none - # @option options [Logger] :logger A custom logger can be used however it must adhere to the Ruby Logger interface, see http://www.ruby-doc.org/stdlib-3.1.1/libdoc/logger/rdoc/Logger.html - # @option options [String] :client_id client ID identifying this connection to other clients - # @option options [String] :auth_url a URL to be used to GET or POST a set of token request params, to obtain a signed token request - # @option options [Hash] :auth_headers a set of application-specific headers to be added to any request made to the +auth_url+ - # @option options [Hash] :auth_params a set of application-specific query params to be added to any request made to the +auth_url+ - # @option options [Symbol] :auth_method (:get) HTTP method to use with +auth_url+, must be either +:get+ or +:post+ - # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required. - # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent - # @option options [Boolean] :query_time when true will query the {https://www.ably.com Ably} system for the current time instead of using the local time - # @option options [Hash] :default_token_params convenience to pass in +token_params+ that will be used as a default for all token requests. See {Auth#create_token_request} - # - # @option options [Integer] :http_open_timeout (4 seconds) timeout in seconds for opening an HTTP connection for all HTTP requests - # @option options [Integer] :http_request_timeout (10 seconds) timeout in seconds for any single complete HTTP request and response - # @option options [Integer] :http_max_retry_count (3) maximum number of fallback host retries for HTTP requests that fail due to network issues or server problems - # @option options [Integer] :http_max_retry_duration (15 seconds) maximum elapsed time in which fallback host retries for HTTP requests will be attempted i.e. if the first default host attempt takes 5s, and then the subsequent fallback retry attempt takes 7s, no further fallback host attempts will be made as the total elapsed time of 12s exceeds the default 10s limit - # - # @option options [Boolean] :fallback_hosts_use_default (false) When true, forces the user of fallback hosts even if a non-default production endpoint is being used - # @option options [Array] :fallback_hosts When an array of fallback hosts are provided, these fallback hosts are always used if a request fails to the primary endpoint. If an empty array is provided, the fallback host functionality is disabled - # @option options [Integer] :fallback_retry_timeout (600 seconds) amount of time in seconds a REST client will continue to use a working fallback host when the primary fallback host has previously failed - # - # @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request. - # @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST - # @option options [Integer] :max_message_size (65536 bytes) Maximum size of all messages when publishing via REST publish() - # @option options [Integer] :max_frame_size (524288 bytes) Maximum size of frame - # - # @return [Ably::Rest::Client] - # - # @example - # # Create a new client authenticating with basic auth using a String object - # client = Ably::PubSub::Server.create_http_client('key.id:secret') - # - # # Construct a client using a Hash object. - # client = Ably::PubSub::Server.create_http_client(key: 'key.id:secret', client_id: 'john') - # - # Applications do not call this constructor: {.new} refuses direct construction, and - # a per-side package reaches it through {Ably::Internal.create_rest_client}. - # - def initialize(options) - raise ArgumentError, 'Options Hash is expected' if options.nil? - - options = options.clone - if options.kind_of?(String) - options = if options.match(Auth::API_KEY_REGEX) - { key: options } - else - { token: options } - end - end - - @agent = options.delete(:agent) || Ably::AGENT - # Additive agent entries (`identifier => version`), appended to the base agent - # string. This is how a package layered on this one (such as ably-pubsub-server) - # declares itself — including the side-declaring entry that MAU classification - # reads — without replacing the base identifiers the way :agent does. - options.delete(:agents).to_h.each do |identifier, version| - @agent = "#{@agent} #{version ? "#{identifier}/#{version}" : identifier}" - end - @realtime_client = options.delete(:realtime_client) - @tls = options.delete_with_default(:tls, true) - @environment = options.delete(:environment) # nil is production - @environment = nil if [:production, 'production'].include?(@environment) - @protocol = options.delete(:protocol) || :msgpack - @debug_http = options.delete(:debug_http) - @log_level = options.delete(:log_level) || ::Logger::WARN - @custom_logger = options.delete(:logger) - @custom_host = options.delete(:rest_host) - @custom_port = options.delete(:port) - @custom_tls_port = options.delete(:tls_port) - @add_request_ids = options.delete(:add_request_ids) - @log_retries_as_info = options.delete(:log_retries_as_info) - @max_message_size = options.delete(:max_message_size) || MAX_MESSAGE_SIZE - @max_frame_size = options.delete(:max_frame_size) || MAX_FRAME_SIZE - @idempotent_rest_publishing = options.delete_with_default(:idempotent_rest_publishing, true) - - if options[:fallback_hosts_use_default] && options[:fallback_hosts] - raise ArgumentError, "fallback_hosts_use_default cannot be set to try when fallback_hosts is also provided" - end - @fallback_hosts = case - when options.delete(:fallback_hosts_use_default) - Ably::FALLBACK_HOSTS - when options_fallback_hosts = options.delete(:fallback_hosts) - options_fallback_hosts - when custom_host || options[:realtime_host] || custom_port || custom_tls_port - [] - when environment - CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES.map { |host| "#{environment}#{host}" } - else - Ably::FALLBACK_HOSTS - end - - options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT - - # Take option keys prefixed with `http_`, remove the http_ and - # check if the option exists in HTTP_DEFAULTS. If so, update http_defaults - @http_defaults = HTTP_DEFAULTS.dup - options.each do |key, val| - if http_key = key[/^http_(.+)/, 1] - # Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout) - val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001) - @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym) - end - end - @http_defaults.freeze - - if @log_level == :none - @custom_logger = Ably::Models::NilLogger.new - else - @log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String) - end - - options.delete(:use_binary_protocol).tap do |use_binary_protocol| - if use_binary_protocol == true - @protocol = :msgpack - elsif use_binary_protocol == false - @protocol = :json - end - end - raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol) - - token_params = options.delete(:default_token_params) || {} - @options = options - init_auth_options = options.select do |key, _| - Auth::AUTH_OPTIONS_KEYS.include?(key.to_s) - end - - @auth = Auth.new(self, token_params, init_auth_options) - @channels = Ably::Rest::Channels.new(self) - @encoders = [] - - options.freeze - - initialize_default_encoders - end - - # Return a REST {Ably::Rest::Channel} for the given name - # - # @param (see Ably::Rest::Channels#get) - # - # @return (see Ably::Rest::Channels#get) - def channel(name, channel_options = {}) - channels.get(name, channel_options) - end - - # Queries the REST /stats API and retrieves your application's usage statistics. Returns a {Ably::Models::PaginatedResult} object, containing an array of {Ably::Models::Stats} objects. See the Stats docs. - # - # @spec RSC6a, RSC6b1, RSC6b2, RSC6b3, RSC6b4 - # - # @param [Hash] options the options for the stats request - # @option options [Integer,Time] :start The time from which stats are retrieved, specified as milliseconds since the Unix epoch. RSC6b1 - # @option options [Integer,Time] :end The time until stats are retrieved, specified as milliseconds since the Unix epoch. RSC6b1 - # @option options [Symbol] :direction The order for which stats are returned in. Valid values are backwards which orders stats from most recent to oldest, or forwards which orders stats from oldest to most recent. The default is backwards. RSC6b2 - # @option options [Integer] :limit An upper limit on the number of stats returned. The default is 100, and the maximum is 1000. RSC6b3 - # @option options [Symbol] :unit minute, hour, day or month. Based on the unit selected, the given start or end times are rounded down to the start of the relevant interval depending on the unit granularity of the query. RSC6b4 - # - # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::Stats} objects. - # - def stats(options = {}) - options = { - :direction => :backwards, - :unit => :minute, - :limit => 100 - }.merge(options) - - [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) } - raise ArgumentError, ":end must be equal to or after :start" if options[:start] && options[:end] && (options[:start] > options[:end]) - - paginated_options = { - coerce_into: 'Ably::Models::Stats' - } - - url = '/stats' - response = get(url, options) - - Ably::Models::PaginatedResult.new(response, url, self, paginated_options) - end - - # Retrieves the time from the Ably service as milliseconds since the Unix epoch. Clients that do not have access - # to a sufficiently well maintained time source and wish to issue Ably {Ably::Models::TokenRequest} with - # a more accurate timestamp should use the {Ably::Rest::Client#queryTime} property instead of this method. - # - # @spec RSC16 - # - # @return [Time] The time as milliseconds since the Unix epoch. - def time - response = get('/time', {}, send_auth_header: false) - - as_time_from_epoch(response.body.first) - end - - # @!attribute [r] use_tls? - # @return [Boolean] True if client is configured to use TLS for all Ably communication - def use_tls? - @tls == true - end - - # Perform an HTTP GET request to the API using configured authentication - # - # @return [Faraday::Response] - # - # @api private - def get(path, params = {}, options = {}) - raw_request(:get, path, params, options) - end - - # Perform an HTTP POST request to the API using configured authentication - # - # @return [Faraday::Response] - # - # @api private - def post(path, params, options = {}) - raw_request(:post, path, params, options) - end - - # Perform an HTTP PUT request to the API using configured authentication - # - # @return [Faraday::Response] - # - # @api private - def put(path, params, options = {}) - raw_request(:put, path, params, options) - end - - # Perform an HTTP DELETE request to the API using configured authentication - # - # @return [Faraday::Response] - # - # @api private - def delete(path, params, options = {}) - raw_request(:delete, path, params, options) - end - - # Makes a REST request to a provided path. This is provided as a convenience for developers who wish to use REST API - # functionality that is either not documented or is not yet included in the public API, without having to directly - # handle features such as authentication, paging, fallback hosts, MsgPack and JSON support. - # - # @spec RSC19 - # - # @param method [Symbol] The request method to use, such as :get, :post. - # @param path [String] The request path. - # @param params [Hash, nil] The parameters to include in the URL query of the request. The parameters depend on the endpoint being queried. See the REST API reference for the available parameters of each endpoint. - # @param body [Hash, nil] The JSON body of the request. - # @param headers [Hash, nil] Additional HTTP headers to include in the request. - # - # @return [Ably::Models::HttpPaginatedResponse<>] An {Ably::Models::HttpPaginatedResponse} object returned by the HTTP request, containing an empty or JSON-encodable object. - def request(method, path, params = {}, body = nil, headers = {}, options = {}) - raise "Method #{method.to_s.upcase} not supported" unless %i(get put patch post delete).include?(method.to_sym) - - response = case method.to_sym - when :get, :delete - reauthorize_on_authorization_failure do - send_request(method, path, params, headers: headers) - end - when :post, :patch, :put - if body.to_json.bytesize > max_frame_size - raise Ably::Exceptions::MaxFrameSizeExceeded.new("Maximum frame size exceeded #{max_frame_size} bytes.") - end - path_with_params = Addressable::URI.new - path_with_params.query_values = params || {} - query = path_with_params.query - reauthorize_on_authorization_failure do - send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers) - end - end - - paginated_options = { - async_blocking_operations: options.delete(:async_blocking_operations), - } - - Ably::Models::HttpPaginatedResponse.new(response, path, self, paginated_options) - - rescue Exceptions::ResourceMissing, Exceptions::ForbiddenRequest, Exceptions::ResourceMissing => e - response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message) - Models::HttpPaginatedResponse.new(response, path, self) - rescue Exceptions::TokenExpired, Exceptions::UnauthorizedRequest => e - response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message) - Models::HttpPaginatedResponse.new(response, path, self) - rescue Exceptions::InvalidRequest, Exceptions::ServerError => e - response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message) - Models::HttpPaginatedResponse.new(response, path, self) - end - - # Retrieves an object that represents the current state of the device as a target for push notifications. - # @spec RSH8 - # @return [Ably::Models::LocalDevice] - # - # @note This is unsupported in the Ruby library - def device - raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable' - end - - # A {Ably::Rest::Push} object. - # @spec RSH7 - # @return [Ably::Rest::Push] - def push - @push ||= Push.new(self) - end - - # @!attribute [r] endpoint - # @return [URI::Generic] Default Ably REST endpoint used for all requests - def endpoint - endpoint_for_host(custom_host || [@environment, DOMAIN].compact.join('-')) - end - - # @!attribute [r] logger - # @return [Logger] The {Ably::Logger} for this client. - # Configure the log_level with the `:log_level` option, refer to {Client#initialize} - def logger - @logger ||= Ably::Logger.new(self, log_level, @custom_logger) - end - - # @!attribute [r] mime_type - # @return [String] Mime type used for HTTP requests - def mime_type - case protocol - when :json - 'application/json' - else - 'application/x-msgpack' - end - end - - # Register a message encoder and decoder that implements Ably::Models::MessageEncoders::Base interface. - # Message encoders are used to encode and decode message payloads automatically. - # @note Encoders and decoders are processed in the order they are added so the first encoder will be given priority when encoding and decoding - # - # @param [Ably::Models::MessageEncoders::Base] encoder - # @return [void] - # - # @api private - def register_encoder(encoder, options = {}) - encoders << Ably::Models::MessageEncoders.encoder_from(encoder, options) - end - - # @!attribute [r] protocol_binary? - # @return [Boolean] True of the transport #protocol communicates with Ably with a binary protocol - def protocol_binary? - protocol == :msgpack - end - - # Connection used to make HTTP requests - # - # @param [Hash] options - # @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see the default {Ably::FALLBACK_HOSTS} - # - # @return [Faraday::Connection] - # - # @api private - def connection(options = {}) - if options[:use_fallback] - fallback_connection - else - @connection ||= Faraday.new(endpoint.to_s, connection_options) - end - end - - # Fallback connection used to make HTTP requests. - # Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback hosts} - # are used (unless custom fallback hosts are provided with fallback_hosts) - # - # @return [Faraday::Connection] - # - # @api private - def fallback_connection - unless defined?(@fallback_connections) && @fallback_connections - @fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) } - end - @fallback_index ||= 0 - - @fallback_connections[@fallback_index % @fallback_connections.count].tap do - @fallback_index += 1 - end - end - - # Allowable duration for an external auth request - # For REST client this defaults to request_timeout - # For Realtime clients this defaults to 250ms less than the realtime_request_timeout - # ensuring an auth failure will be triggered before the realtime request timeout fires - # which would lead to a misleading error message (connection timeout as opposed to auth request timeout) - # @api private - def auth_request_timeout - if @realtime_client - @realtime_client.connection.defaults.fetch(:realtime_request_timeout) - 0.25 - else - http_defaults.fetch(:request_timeout) - end - end - - # If the primary host endpoint fails, and a subsequent fallback host succeeds, the fallback - # host that succeeded is used for +ClientOption+ +fallback_retry_timeout+ seconds to avoid - # retries to known failing hosts for a short period of time. - # See https://github.com/ably/docs/pull/554, spec id #RSC15f - # - # @return [nil, String] Returns nil (falsey) if the primary host is being used, or the currently used host if a fallback host is currently preferred - def using_preferred_fallback_host? - if preferred_fallback_connection && (preferred_fallback_connection.fetch(:expires_at) > Time.now) - preferred_fallback_connection.fetch(:connection_object).host - end - end - - private - - attr_reader :preferred_fallback_connection - - # See #using_preferred_fallback_host? for context - def set_preferred_fallback_connection(connection) - @preferred_fallback_connection = if connection == @connection - # If the succeeded connection is in fact the primary connection (tried after a failed fallback) - # then clear the preferred fallback connection - nil - else - { - expires_at: Time.now + options.fetch(:fallback_retry_timeout), - connection_object: connection, - } - end - end - - def get_preferred_fallback_connection_object - preferred_fallback_connection.fetch(:connection_object) if using_preferred_fallback_host? - end - - def raw_request(method, path, params = {}, options = {}) - options = options.clone - if options.delete(:disable_automatic_reauthorize) == true - send_request(method, path, params, options) - else - reauthorize_on_authorization_failure do - send_request(method, path, params, options) - end - end - end - - # Sends HTTP request to connection end point - # Connection failures will automatically be reattempted until thresholds are met - def send_request(method, path, params, options) - max_retry_count = http_defaults.fetch(:max_retry_count) - max_retry_duration = http_defaults.fetch(:max_retry_duration) - requested_at = Time.now - retry_count = 0 - retry_sequence_id = nil - request_id = SecureRandom.urlsafe_base64(10) if add_request_ids - - preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object - - begin - use_fallback = can_fallback_to_alternate_ably_host? && (retry_count > 0) - - conn = if preferred_fallback_connection_for_first_request - case retry_count - when 0 - preferred_fallback_connection_for_first_request - when 1 - # Ensure the root host is used first if the preferred fallback fails, see #RSC15f - connection(use_fallback: false) - end - end || connection(use_fallback: use_fallback) # default to normal connection selection process if not preferred connection set - - conn.send(method, path, params) do |request| - if add_request_ids - request.params[:request_id] = request_id - request.options.context = {} if request.options.context.nil? - request.options.context[:request_id] = request_id - end - if options[:qs_params] - request.params.merge!(options[:qs_params]) - end - unless options[:send_auth_header] == false - request.headers[:authorization] = auth.auth_header - # RSA7e2 - options[:headers].to_h.merge(auth.extra_auth_headers).map do |key, val| - request.headers[key] = val - end - end - end.tap do - if retry_count > 0 - retry_log_severity = log_retries_as_info ? :info : :warn - logger.public_send(retry_log_severity) do - "Ably::Rest::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \ - " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)" - end - set_preferred_fallback_connection conn - end - end - - rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error - retry_sequence_id ||= SecureRandom.urlsafe_base64(4) - time_passed = Time.now - requested_at - - if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration) - retry_count += 1 - retry_log_severity = log_retries_as_info ? :info : :warn - logger.public_send(retry_log_severity) { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" } - retry - end - - retry_log_severity = log_retries_as_info ? :info : :error - logger.public_send(retry_log_severity) do - "Ably::Rest::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \ - " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)" - end - - case error - when Faraday::TimeoutError - raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id }) - when *FARADAY_CLIENT_OR_SERVER_ERRORS - # request_id is also available in the request context - raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id }) - else - raise error - end - end - end - - def reauthorize_on_authorization_failure - yield - rescue Ably::Exceptions::TokenExpired => e - if auth.token_renewable? - auth.authorize - yield - else - raise e - end - end - - def endpoint_for_host(host) - port = if use_tls? - custom_tls_port - else - custom_port - end - - raise ArgumentError, "Custom port must be an Integer or nil" if port && !port.kind_of?(Integer) - - options = { - scheme: use_tls? ? 'https' : 'http', - host: host - } - options.merge!(port: port) if port - - URI::Generic.build(options) - end - - # Return a Hash of connection options to initiate the Faraday::Connection with - # - # @return [Hash] - def connection_options - @connection_options ||= { - builder: middleware, - headers: { - content_type: mime_type, - accept: mime_type, - user_agent: user_agent, - 'X-Ably-Version' => Ably::PROTOCOL_VERSION, - 'Ably-Agent' => agent - }, - request: { - open_timeout: http_defaults.fetch(:open_timeout), - timeout: http_defaults.fetch(:request_timeout) - } - } - end - - # Return a Faraday middleware stack to initiate the Faraday::RackBuilder with - # - # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ - def middleware - @middleware ||= Faraday::RackBuilder.new do |builder| - setup_outgoing_middleware builder - - # Raise exceptions if response code is invalid - builder.use Ably::Rest::Middleware::Exceptions - - setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true - - # Set Faraday's HTTP adapter with support for HTTP/2 - builder.adapter :typhoeus, http_version: :httpv2_0 - end - end - - def can_fallback_to_alternate_ably_host? - fallback_hosts && !fallback_hosts.empty? - end - - def initialize_default_encoders - Ably::Models::MessageEncoders.register_default_encoders self, binary_protocol: protocol == :msgpack - end - end - end -end diff --git a/core/lib/ably/rest/middleware/encoder.rb b/core/lib/ably/rest/middleware/encoder.rb deleted file mode 100644 index fe6e38b70..000000000 --- a/core/lib/ably/rest/middleware/encoder.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'faraday' -require 'json' - -module Ably - module Rest - module Middleware - # Encode the body of the message according to the mime type - class Encoder < Faraday::Middleware - CONTENT_TYPE = 'Content-Type'.freeze unless defined? CONTENT_TYPE - - def call(env) - encode env if env.body - @app.call env - end - - private - def encode(env) - env.body = case request_type(env) - when 'application/x-msgpack' - to_msgpack(env.body) - when 'application/json', '', nil - env.request_headers[CONTENT_TYPE] = 'application/json' - to_json(env.body) - else - env.body - end - end - - def to_msgpack(body) - body.to_msgpack - end - - def to_json(body) - if body.kind_of?(String) - body - else - body.to_json - end - end - - def request_type(env) - type = env.request_headers[CONTENT_TYPE].to_s - type = type.split(';', 2).first if type.index(';') - type - end - end - end - end -end diff --git a/core/lib/ably/rest/middleware/exceptions.rb b/core/lib/ably/rest/middleware/exceptions.rb deleted file mode 100644 index e64d29e85..000000000 --- a/core/lib/ably/rest/middleware/exceptions.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'faraday' -require 'json' - -module Ably - module Rest - module Middleware - # HTTP exceptions raised by Ably due to an error status code - # Ably returns JSON/Msgpack error codes and messages so include this if possible in the exception messages - class Exceptions < Faraday::Middleware - def on_complete(env) - if env.status >= 400 - error_status_code = env.status - error_code = nil - - if env.body.kind_of?(Hash) - error = env.body.fetch('error', {}) - error_status_code = error['statusCode'].to_i if error['statusCode'] - error_code = error['code'].to_i if error['code'] - - if error - message = "#{error['message']} (status: #{error_status_code}, code: #{error_code})" - else - message = env.body - end - else - message = env.body - end - - message = 'Unknown server error' if message.to_s.strip == '' - request_id = env.request.context[:request_id] if env.request.context - exception_args = [message, error_status_code, error_code, nil, { request_id: request_id }] - - if env.status >= 500 - raise Ably::Exceptions::ServerError.new(*exception_args) - elsif env.status == 401 - if Ably::Exceptions::TOKEN_EXPIRED_CODE.include?(error_code) - raise Ably::Exceptions::TokenExpired.new(*exception_args) - else - raise Ably::Exceptions::UnauthorizedRequest.new(*exception_args) - end - elsif env.status == 403 - raise Ably::Exceptions::ForbiddenRequest.new(*exception_args) - elsif env.status == 404 - raise Ably::Exceptions::ResourceMissing.new(*exception_args) - else - raise Ably::Exceptions::InvalidRequest.new(*exception_args) - end - end - end - end - end - end -end diff --git a/core/lib/ably/rest/middleware/external_exceptions.rb b/core/lib/ably/rest/middleware/external_exceptions.rb deleted file mode 100644 index 626a0b523..000000000 --- a/core/lib/ably/rest/middleware/external_exceptions.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'faraday' - -module Ably - module Rest - module Middleware - # HTTP exceptions raised due to a status code error on a 3rd party site - # Used by auth calls - class ExternalExceptions < Faraday::Middleware - def on_complete(env) - if env.status >= 400 - error_status_code = env.status - message = "Error #{error_status_code}: #{(env.body || '')[0...200]}" - - if error_status_code >= 500 - raise Ably::Exceptions::ServerError, message - else - raise Ably::Exceptions::InvalidRequest, message - end - end - end - end - end - end -end diff --git a/core/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb b/core/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb deleted file mode 100644 index a5c271e28..000000000 --- a/core/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'faraday' -require 'json' - -module Ably - module Rest - module Middleware - class FailIfUnsupportedMimeType < Faraday::Middleware - def on_complete(env) - unless env.response_headers['Ably-Middleware-Parsed'] == true - # Ignore empty body with success status code for no body response - return if env.body.to_s.empty? && env.status == 204 - - unless (500..599).include?(env.status) - raise Ably::Exceptions::InvalidResponseBody, - "Content Type #{env.response_headers['Content-Type']} is not supported by this client library" - end - end - end - end - end - end -end diff --git a/core/lib/ably/rest/middleware/logger.rb b/core/lib/ably/rest/middleware/logger.rb deleted file mode 100644 index 2a33b2f2d..000000000 --- a/core/lib/ably/rest/middleware/logger.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'faraday' - -module Ably - module Rest - module Middleware - class Logger < Faraday::Middleware - extend Forwardable - - def initialize(app, logger = nil) - super(app) - @logger = logger || begin - require 'logger' - ::Logger.new(STDOUT) - end - end - - def_delegators :@logger, :debug, :info, :warn, :error, :fatal - - def call(env) - debug { "=> URL: #{env.method} #{env.url}, Headers: #{dump_headers env.request_headers}" } - debug { "=> Body: #{body_for(env)}" } - super - end - - def on_complete(env) - debug "<= Status: #{env.status}, Headers: #{dump_headers env.response_headers}" - debug "<= Body: #{body_for(env)}" - end - - private - def dump_headers(headers) - headers.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") - end - - def body_for(env) - return '' if !env.body || env.body.empty? - - if env.request_headers['Content-Type'] == 'application/x-msgpack' - MessagePack.unpack(env.body) - else - env.body - end - - rescue StandardError - readable_body(env.body) - end - - def readable_body(body) - if body.respond_to?(:encoding) && body.encoding == Encoding::ASCII_8BIT - body.unpack('H*') - else - body - end - end - end - end - end -end diff --git a/core/lib/ably/rest/middleware/parse_json.rb b/core/lib/ably/rest/middleware/parse_json.rb deleted file mode 100644 index 71ae938cd..000000000 --- a/core/lib/ably/rest/middleware/parse_json.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'faraday' -require 'json' - -module Ably - module Rest - module Middleware - class ParseJson < Faraday::Middleware - def on_complete(env) - if env.response_headers['Content-Type'] == 'application/json' - env.body = parse(env.body) unless env.response_headers['Ably-Middleware-Parsed'] == true - env.response_headers['Ably-Middleware-Parsed'] = true - end - end - - def parse(body) - if body.length > 0 - JSON.parse(body) - else - body - end - rescue JSON::ParserError => e - raise Ably::Exceptions::InvalidResponseBody, "Expected JSON response: #{e.message}" - end - end - end - end -end diff --git a/core/lib/ably/rest/middleware/parse_message_pack.rb b/core/lib/ably/rest/middleware/parse_message_pack.rb deleted file mode 100644 index de22c6ee9..000000000 --- a/core/lib/ably/rest/middleware/parse_message_pack.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'faraday' -require 'msgpack' - -module Ably - module Rest - module Middleware - class ParseMessagePack < Faraday::Middleware - def on_complete(env) - if env.response_headers['Content-Type'] == 'application/x-msgpack' - env.body = parse(env.body) unless env.response_headers['Ably-Middleware-Parsed'] == true - env.response_headers['Ably-Middleware-Parsed'] = true - end - rescue Ably::Exceptions::InvalidResponseBody => e - debug_info = { - method: env.method, - url: env.url, - base64_body: base64_body(env.body), - response_headers: env.response_headers - } - raise Ably::Exceptions::InvalidResponseBody, "#{e.message}\nRequest env: #{debug_info}" - end - - def parse(body) - if body.length > 0 - MessagePack.unpack(body) - else - body - end - rescue MessagePack::UnknownExtTypeError => e - raise Ably::Exceptions::InvalidResponseBody, "MessagePack::UnknownExtTypeError body could not be decoded: #{e.message}. Got Base64:\n#{base64_body(body)}" - rescue MessagePack::MalformedFormatError => e - raise Ably::Exceptions::InvalidResponseBody, "MessagePack::MalformedFormatError body could not be decoded: #{e.message}. Got Base64:\n#{base64_body(body)}" - end - - def base64_body(body) - Base64.encode64(body) - rescue => err - "[#{err.message}! Could not base64 encode body: '#{body}']" - end - end - end - end -end diff --git a/core/lib/ably/rest/presence.rb b/core/lib/ably/rest/presence.rb deleted file mode 100644 index afb701775..000000000 --- a/core/lib/ably/rest/presence.rb +++ /dev/null @@ -1,115 +0,0 @@ -module Ably - module Rest - # Enables the retrieval of the current and historic presence set for a channel. - # - class Presence - include Ably::Modules::Conversions - - # {Ably::Rest::Client} for this Presence object - # - # @return {Ably::Rest::Client} - # - # @private - attr_reader :client - - # {Ably::Rest::Channel} this Presence object is associated with - # - # @return [Ably::Rest::Channel] - # - attr_reader :channel - - # Initialize a new Presence object - # - # @param client [Ably::Rest::Client] - # @param channel [Channel] The channel object - # - def initialize(client, channel) - @client = client - @channel = channel - end - - # Retrieves the current members present on the channel and the metadata for each member, such as their - # {Ably::Models::PresenceMessage::ACTION} and ID. Returns a {Ably::Models::PaginatedResult} object, - # containing an array of {Ably::Models::PresenceMessage} objects. - # - # @spec RSPa, RSP3a, RSP3a2, RSP3a3 - # - # @param [Hash] options the options for the set of members present - # @option options [Integer] :limit An upper limit on the number of messages returned. The default is 100, and the maximum is 1000. (RSP3a) - # @option options [String] :client_id Filters the list of returned presence members by a specific client using its ID. (RSP3a2) - # @option options [String] :connection_id Filters the list of returned presence members by a specific connection using its ID. (RSP3a3) - # - # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::PresenceMessage} objects. - # - def get(options = {}) - options = options = { - :limit => 100 - }.merge(options) - - paginated_options = { - coerce_into: 'Ably::Models::PresenceMessage', - async_blocking_operations: options.delete(:async_blocking_operations), - } - - # The REST API names these query params clientId and connectionId (RSP3a2, RSP3a3), - # so the idiomatic snake_case options have to be converted before they are sent. - response = client.get(base_path, IdiomaticRubyWrapper(options).as_json) - - Ably::Models::PaginatedResult.new(response, base_path, client, paginated_options) do |presence_message| - presence_message.tap do |message| - decode_message message - end - end - end - - # Retrieves a {Ably::Models::PaginatedResult} object, containing an array of historical {Ably::Models::PresenceMessage} - # objects for the channel. If the channel is configured to persist messages, then presence messages can be retrieved - # from history for up to 72 hours in the past. If not, presence messages can only be retrieved from history for up to two minutes in the past. - # - # @spec RSP4a - # - # @param [Hash] options the options for the message history request - # @option options [Integer,Time] :start The time from which messages are retrieved, specified as milliseconds since the Unix epoch. (RSP4b1) - # @option options [Integer,Time] :end The time until messages are retrieved, specified as milliseconds since the Unix epoch. (RSP4b1) - # @option options [Symbol] :direction The order for which messages are returned in. Valid values are backwards which orders messages from most recent to oldest, or forwards which orders messages from oldest to most recent. The default is backwards. (RSP4b2) - # @option options [Integer] :limit An upper limit on the number of messages returned. The default is 100, and the maximum is 1000. (RSP4b3) - # - # @return [Ably::Models::PaginatedResult] A {Ably::Models::PaginatedResult} object containing an array of {Ably::Models::PresenceMessage} objects. - # - def history(options = {}) - url = "#{base_path}/history" - options = options = { - :direction => :backwards, - :limit => 100 - }.merge(options) - - [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) } - raise ArgumentError, ":end must be equal to or after :start" if options[:start] && options[:end] && (options[:start] > options[:end]) - - paginated_options = { - coerce_into: 'Ably::Models::PresenceMessage', - async_blocking_operations: options.delete(:async_blocking_operations), - } - - response = client.get(url, options) - - Ably::Models::PaginatedResult.new(response, url, client, paginated_options) do |presence_message| - presence_message.tap do |message| - decode_message message - end - end - end - - private - def base_path - "/channels/#{URI.encode_www_form_component(channel.name)}/presence" - end - - def decode_message(presence_message) - presence_message.decode client.encoders, channel.options - rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e - client.logger.error { "Decoding Error on presence channel '#{channel.name}', presence message client_id '#{presence_message.client_id}'. #{e.class.name}: #{e.message}" } - end - end - end -end diff --git a/core/lib/ably/rest/push.rb b/core/lib/ably/rest/push.rb deleted file mode 100644 index 7d33bb3a9..000000000 --- a/core/lib/ably/rest/push.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'ably/rest/push/admin' - -module Ably - module Rest - # Class providing push notification functionality - class Push - include Ably::Modules::Conversions - - # @private - attr_reader :client - - def initialize(client) - @client = client - end - - # Admin features for push notifications like managing devices and channel subscriptions - # - # @return [Ably::Rest::Push::Admin] - # - def admin - @admin ||= Admin.new(self) - end - end - end -end diff --git a/server/README.md b/server/README.md index eecd738f6..b00e34e46 100644 --- a/server/README.md +++ b/server/README.md @@ -14,7 +14,7 @@ gem 'ably-pubsub-server' ## Usage The factory functions are the only entry points. The client classes refuse direct -construction — `Ably::Rest::Client.new` and `Ably::Realtime::Client.new` raise +construction — `Ably::PubSub::Http::Client.new` and `Ably::PubSub::Realtime::Client.new` raise `Ably::Exceptions::DirectConstructionNotSupported` — because the package a client is created from is what declares its side, and a directly constructed client declares none. diff --git a/server/lib/ably/pubsub/server.rb b/server/lib/ably/pubsub/server.rb index ed87bab5f..d3004f301 100644 --- a/server/lib/ably/pubsub/server.rb +++ b/server/lib/ably/pubsub/server.rb @@ -25,20 +25,20 @@ module Server class << self # Creates a stateless HTTP (REST) client declaring the server side. # - # Accepts everything {Ably::Rest::Client#initialize} accepts: an options Hash, + # Accepts everything {Ably::PubSub::Http::Client#initialize} accepts: an options Hash, # an API key String, or a token String. # - # @return [Ably::Rest::Client] + # @return [Ably::PubSub::Http::Client] def create_http_client(options) - Ably::Internal.create_rest_client(options_with_side_agent(options)) + Ably::Internal.create_http_client(options_with_side_agent(options)) end # Creates a stateful realtime client declaring the server side. # - # Accepts everything {Ably::Realtime::Client#initialize} accepts: an options Hash, + # Accepts everything {Ably::PubSub::Realtime::Client#initialize} accepts: an options Hash, # an API key String, or a token String. # - # @return [Ably::Realtime::Client] + # @return [Ably::PubSub::Realtime::Client] def create_realtime_client(options) Ably::Internal.create_realtime_client(options_with_side_agent(options)) end diff --git a/spec/acceptance/rest/auth_spec.rb b/spec/acceptance/http/auth_spec.rb similarity index 98% rename from spec/acceptance/rest/auth_spec.rb rename to spec/acceptance/http/auth_spec.rb index d624fe9c2..9beb218d0 100644 --- a/spec/acceptance/rest/auth_spec.rb +++ b/spec/acceptance/http/auth_spec.rb @@ -29,7 +29,7 @@ def hmac_for(token_request_attributes, secret) let(:default_options) { { environment: environment, protocol: protocol } } let(:client_options) { default_options.merge(key: api_key) } let(:client) do - Ably::Internal.create_rest_client(client_options) + Ably::Internal.create_http_client(client_options) end let(:auth) { client.auth } let(:content_type) do @@ -517,7 +517,7 @@ def coerce_if_time_value(field_name, value, params = {}) end context 'that returns a Token string' do - let(:second_client) { Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) } + let(:second_client) { Ably::Internal.create_http_client(key: api_key, environment: environment, protocol: protocol) } let(:token) { second_client.auth.request_token.token } let!(:token_details) do @@ -798,7 +798,7 @@ def coerce_if_time_value(field_name, value, params = {}) @block_called = 0 end - let(:token_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, default_token_params: { ttl: 3 })) } + let(:token_client) { Ably::Internal.create_http_client(default_options.merge(key: api_key, default_token_params: { ttl: 3 })) } let(:client_options) { default_options.merge(token: token_client.auth.request_token.token, auth_callback: lambda do |token_params| @block_called += 1 @@ -819,7 +819,7 @@ def coerce_if_time_value(field_name, value, params = {}) context 'with an explicit ClientOptions client_id' do let(:client_id) { random_str } let(:client_options) { default_options.merge(auth_callback: lambda { |token_params| auth_token_object }, client_id: client_id) } - let(:auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: 'invalid')) } + let(:auth_client) { Ably::Internal.create_http_client(default_options.merge(key: api_key, client_id: 'invalid')) } context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do let(:auth_token_object) { auth_client.auth.request_token } @@ -861,7 +861,7 @@ def coerce_if_time_value(field_name, value, params = {}) it 'returns a TokenRequest that can be passed to a client that can use it for authentication without an API key' do auth_callback = proc { |token_params| subject } - client_without_api_key = Ably::Internal.create_rest_client(default_options.merge(auth_callback: auth_callback)) + client_without_api_key = Ably::Internal.create_http_client(default_options.merge(auth_callback: auth_callback)) expect(client_without_api_key.auth).to be_using_token_auth expect { client_without_api_key.auth.authorize }.to_not raise_error end @@ -922,7 +922,7 @@ def coerce_if_time_value(field_name, value, params = {}) it 'uses these capabilities when Ably issues an actual token' do auth_callback = lambda { |token_params| subject } - client_without_api_key = Ably::Internal.create_rest_client(default_options.merge(auth_callback: auth_callback)) + client_without_api_key = Ably::Internal.create_http_client(default_options.merge(auth_callback: auth_callback)) client_without_api_key.auth.authorize expect(client_without_api_key.auth.current_token_details.capability).to eql(capability) end @@ -939,7 +939,7 @@ def coerce_if_time_value(field_name, value, params = {}) end context 'when required fields are missing' do - let(:client) { Ably::Internal.create_rest_client(auth_url: 'http://example.com', protocol: protocol) } + let(:client) { Ably::Internal.create_http_client(auth_url: 'http://example.com', protocol: protocol) } it 'should raise an exception if key secret is missing' do expect { auth.create_token_request({}, key_name: 'name') }.to raise_error Ably::Exceptions::TokenRequestFailed @@ -1033,7 +1033,7 @@ def coerce_if_time_value(field_name, value, params = {}) auth_callback = lambda do |callback| auth.create_token_request(token_attributes) end - client = Ably::Internal.create_rest_client(auth_callback: auth_callback, environment: environment, protocol: protocol) + client = Ably::Internal.create_http_client(auth_callback: auth_callback, environment: environment, protocol: protocol) client.auth.authorize end end @@ -1053,7 +1053,7 @@ def coerce_if_time_value(field_name, value, params = {}) end let(:token) { token_details.token } let(:token_auth_client) do - Ably::Internal.create_rest_client(token: token, environment: environment, protocol: protocol) + Ably::Internal.create_http_client(token: token, environment: environment, protocol: protocol) end it 'authenticates successfully using the provided :token' do @@ -1186,10 +1186,10 @@ def coerce_if_time_value(field_name, value, params = {}) context 'when :client_id is provided in a token' do let(:client_id) { '123' } let(:token) do - Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol).auth.request_token(client_id: client_id) + Ably::Internal.create_http_client(key: api_key, environment: environment, protocol: protocol).auth.request_token(client_id: client_id) end let(:client) do - Ably::Internal.create_rest_client(token: token, environment: environment, protocol: protocol) + Ably::Internal.create_http_client(token: token, environment: environment, protocol: protocol) end specify '#client_id contains the client_id' do @@ -1199,7 +1199,7 @@ def coerce_if_time_value(field_name, value, params = {}) end describe '#client_id_validated?' do - let(:auth) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)).auth } + let(:auth) { Ably::Internal.create_http_client(default_options.merge(key: api_key)).auth } context 'when using basic auth' do let(:client_options) { default_options.merge(key: api_key) } @@ -1293,7 +1293,7 @@ def coerce_if_time_value(field_name, value, params = {}) context 'when using JWT' do let(:auth_url) { 'https://echo.ably.io/createJWT' } let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body } - let(:client) { Ably::Internal.create_rest_client(token: token, environment: environment, protocol: protocol) } + let(:client) { Ably::Internal.create_http_client(token: token, environment: environment, protocol: protocol) } it 'authenticates correctly using the JWT token generated by the echo server' do expect(client.stats).to_not be_nil() @@ -1317,7 +1317,7 @@ def coerce_if_time_value(field_name, value, params = {}) # RSA4f, RSA8c context 'when the token requested is returned with application/jwt content type' do - let(:auth_rest_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } + let(:auth_rest_client) { Ably::Internal.create_http_client(default_options.merge(key: api_key)) } let(:auth_params) { { keyName: key_name, keySecret: key_secret, returnType: 'jwt' } } let(:token) { auth_rest_client.auth.request_token({ }, { auth_url: auth_url, auth_params: auth_params }).token } it 'authenticates correctly and pulls stats' do diff --git a/spec/acceptance/rest/base_spec.rb b/spec/acceptance/http/base_spec.rb similarity index 95% rename from spec/acceptance/rest/base_spec.rb rename to spec/acceptance/http/base_spec.rb index b08c06070..e696d4e6d 100644 --- a/spec/acceptance/rest/base_spec.rb +++ b/spec/acceptance/http/base_spec.rb @@ -1,13 +1,13 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest do +describe Ably::PubSub::Http do describe 'transport protocol' do include Ably::Modules::Conversions let(:client_options) { {} } let(:client) do - Ably::Internal.create_rest_client(client_options.merge(key: 'appid.keyuid:keysecret', log_retries_as_info: true)) + Ably::Internal.create_http_client(client_options.merge(key: 'appid.keyuid:keysecret', log_retries_as_info: true)) end let(:now) { Time.now - 1000 } @@ -67,13 +67,13 @@ vary_by_protocol do let(:client) do - Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) + Ably::Internal.create_http_client(key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) end describe 'failed requests' do context 'due to invalid Auth' do it 'should raise an UnauthorizedRequest exception with a valid error message and code' do - invalid_client = Ably::Internal.create_rest_client(key: 'appid.keyuid:keysecret', environment: environment) + invalid_client = Ably::Internal.create_http_client(key: 'appid.keyuid:keysecret', environment: environment) expect { invalid_client.channel('test').publish('foo', 'choo') }.to raise_error do |error| expect(error).to be_a(Ably::Exceptions::UnauthorizedRequest) expect(error.code).to eql(40101) @@ -156,7 +156,7 @@ end context 'when NOT auth#token_renewable?' do - let(:client) { Ably::Internal.create_rest_client(token: 'token ID cannot be used to create a new token', environment: environment, protocol: protocol) } + let(:client) { Ably::Internal.create_http_client(token: 'token ID cannot be used to create a new token', environment: environment, protocol: protocol) } it 'should raise an TokenExpired exception' do client.channel(channel).publish('evt', 'msg') diff --git a/spec/acceptance/rest/channel_spec.rb b/spec/acceptance/http/channel_spec.rb similarity index 95% rename from spec/acceptance/rest/channel_spec.rb rename to spec/acceptance/http/channel_spec.rb index 924ebae48..cf02207b0 100644 --- a/spec/acceptance/rest/channel_spec.rb +++ b/spec/acceptance/http/channel_spec.rb @@ -1,14 +1,14 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest::Channel do +describe Ably::PubSub::Http::Channel do include Ably::Modules::Conversions vary_by_protocol do let(:default_options) { { key: api_key, environment: environment, protocol: protocol, max_frame_size: max_frame_size, max_message_size: max_message_size, idempotent_rest_publishing: false } } let(:client_options) { default_options } let(:client) do - Ably::Internal.create_rest_client(client_options) + Ably::Internal.create_http_client(client_options) end let(:max_message_size) { nil } let(:max_frame_size) { nil } @@ -62,8 +62,8 @@ end it 'publishes an array of messages in one HTTP request' do - expect(client.max_message_size).to eq(Ably::Rest::Client::MAX_MESSAGE_SIZE) - expect(messages.sum(&:size) < Ably::Rest::Client::MAX_MESSAGE_SIZE).to eq(true) + expect(client.max_message_size).to eq(Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE) + expect(messages.sum(&:size) < Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE).to eq(true) expect(client).to receive(:post).once.and_call_original expect(channel.publish(messages)).to eql(true) @@ -75,8 +75,8 @@ context 'with an array of Message objects' do context 'when max_message_size and max_frame_size is not set' do before do - expect(client.max_message_size).to eq(Ably::Rest::Client::MAX_MESSAGE_SIZE) - expect(client.max_frame_size).to eq(Ably::Rest::Client::MAX_FRAME_SIZE) + expect(client.max_message_size).to eq(Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE) + expect(client.max_frame_size).to eq(Ably::PubSub::Http::Client::MAX_FRAME_SIZE) end context 'and messages size (130 bytes) is smaller than the max_message_size' do @@ -114,7 +114,7 @@ before do expect(client.max_message_size).to eq(max_message_size) - expect(client.max_frame_size).to eq(Ably::Rest::Client::MAX_FRAME_SIZE) + expect(client.max_frame_size).to eq(Ably::PubSub::Http::Client::MAX_FRAME_SIZE) end context 'and messages size (130 bytes) is smaller than the max_message_size' do @@ -232,9 +232,9 @@ context 'identified clients' do context 'when authenticated with a wildcard client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: '*') } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: '*') } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Internal.create_rest_client(client_options) } + let(:client) { Ably::Internal.create_http_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a valid client_id in the message' do @@ -263,9 +263,9 @@ end context 'when authenticated with a Token string with an implicit client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid').token } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: 'valid').token } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Internal.create_rest_client(client_options) } + let(:client) { Ably::Internal.create_http_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'without having a confirmed identity' do @@ -296,9 +296,9 @@ end context 'when authenticated with TokenDetails with a valid client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid') } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: 'valid') } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Internal.create_rest_client(client_options) } + let(:client) { Ably::Internal.create_http_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a valid client_id in the message' do @@ -333,9 +333,9 @@ end context 'when anonymous and no client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: nil) } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: nil) } let(:client_options) { default_options.merge(key: nil, token: token) } - let(:client) { Ably::Internal.create_rest_client(client_options) } + let(:client) { Ably::Internal.create_http_client(client_options) } let(:channel) { client.channels.get(channel_name) } context 'with a client_id in the message' do @@ -592,7 +592,7 @@ let(:channel) { client.channel(channel_name) } it 'returns a REST Presence object' do - expect(channel.presence).to be_a(Ably::Rest::Presence) + expect(channel.presence).to be_a(Ably::PubSub::Http::Presence) end end diff --git a/spec/acceptance/rest/channels_spec.rb b/spec/acceptance/http/channels_spec.rb similarity index 91% rename from spec/acceptance/rest/channels_spec.rb rename to spec/acceptance/http/channels_spec.rb index 82efc17d8..bca13dae0 100644 --- a/spec/acceptance/rest/channels_spec.rb +++ b/spec/acceptance/http/channels_spec.rb @@ -1,10 +1,10 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest::Channels do +describe Ably::PubSub::Http::Channels do shared_examples 'a channel' do it 'returns a channel object' do - expect(channel).to be_a Ably::Rest::Channel + expect(channel).to be_a Ably::PubSub::Http::Channel expect(channel.name).to eq(channel_name) end @@ -15,7 +15,7 @@ vary_by_protocol do let(:client) do - Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) + Ably::Internal.create_http_client(key: api_key, environment: environment, protocol: protocol) end let(:channel_name) { random_str } let(:options) { { key: 'value' } } @@ -56,7 +56,7 @@ expect(original_channel.options.to_h).to_not include(:encrypted) new_channel = client.channels.get(channel_name, new_channel_options) - expect(new_channel).to be_a(Ably::Rest::Channel) + expect(new_channel).to be_a(Ably::PubSub::Http::Channel) expect(new_channel.options[:encrypted]).to eql(true) end end @@ -67,7 +67,7 @@ it 'returns the existing channel without modifying the channel options' do expect(original_channel.options.to_h).to eq(options) new_channel = client.channels.get(channel_name) - expect(new_channel).to be_a(Ably::Rest::Channel) + expect(new_channel).to be_a(Ably::PubSub::Http::Channel) expect(original_channel.options.to_h).to eq(options) end end diff --git a/spec/acceptance/rest/client_spec.rb b/spec/acceptance/http/client_spec.rb similarity index 96% rename from spec/acceptance/rest/client_spec.rb rename to spec/acceptance/http/client_spec.rb index 2d0c89b6c..98a69c61c 100644 --- a/spec/acceptance/rest/client_spec.rb +++ b/spec/acceptance/http/client_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' require 'webrick' -describe Ably::Rest::Client do +describe Ably::PubSub::Http::Client do vary_by_protocol do let(:default_options) { { environment: environment, protocol: protocol, log_retries_as_info: true } } let(:client_options) { default_options } - let(:client) { Ably::Internal.create_rest_client(client_options) } + let(:client) { Ably::Internal.create_http_client(client_options) } - http_defaults = Ably::Rest::Client::HTTP_DEFAULTS + http_defaults = Ably::PubSub::Http::Client::HTTP_DEFAULTS def encode64(text) Base64.urlsafe_encode64(text) @@ -20,7 +20,7 @@ def encode64(text) let(:token_request) { client.auth.create_token_request({}, key_name: key_name, key_secret: key_secret, client_id: client_id) } context 'with only an API key' do - let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: api_key)) } + let(:client) { Ably::Internal.create_http_client(client_options.merge(key: api_key)) } it 'uses basic authentication' do expect(client.auth).to be_using_basic_auth @@ -28,7 +28,7 @@ def encode64(text) end context 'with an invalid API key' do - let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: 'app.key:secret', log_level: :fatal)) } + let(:client) { Ably::Internal.create_http_client(client_options.merge(key: 'app.key:secret', log_level: :fatal)) } it 'logs an entry with a help href url matching the code #TI5' do begin @@ -41,7 +41,7 @@ def encode64(text) end context 'with an explicit string :token' do - let(:client) { Ably::Internal.create_rest_client(client_options.merge(token: random_str)) } + let(:client) { Ably::Internal.create_http_client(client_options.merge(token: random_str)) } it 'uses token authentication' do expect(client.auth).to be_using_token_auth @@ -49,7 +49,7 @@ def encode64(text) end context 'with :use_token_auth set to true' do - let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: api_key, use_token_auth: true)) } + let(:client) { Ably::Internal.create_http_client(client_options.merge(key: api_key, use_token_auth: true)) } it 'uses token authentication' do expect(client.auth).to be_using_token_auth @@ -57,7 +57,7 @@ def encode64(text) end context 'with a non string :client_id' do - let(:client) { Ably::Internal.create_rest_client(client_options.merge(key: api_key, client_id: 1)) } + let(:client) { Ably::Internal.create_http_client(client_options.merge(key: api_key, client_id: 1)) } it 'raises an ArgumentError' do expect { client.auth }.to raise_error ArgumentError, /client_id.*String/ @@ -66,12 +66,12 @@ def encode64(text) context 'with an invalid wildcard "*" :client_id' do it 'raises an exception' do - expect { Ably::Internal.create_rest_client(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError + expect { Ably::Internal.create_http_client(client_options.merge(key: api_key, client_id: '*')) }.to raise_error ArgumentError end end context 'with an :auth_callback lambda' do - let(:client) { Ably::Internal.create_rest_client(client_options.merge(auth_callback: lambda { |token_params| token_request })) } + let(:client) { Ably::Internal.create_http_client(client_options.merge(auth_callback: lambda { |token_params| token_request })) } it 'calls the auth lambda to get a new token' do expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details } @@ -85,7 +85,7 @@ def encode64(text) context 'with :default_token_params' do let(:client) do - Ably::Internal.create_rest_client(client_options.merge( + Ably::Internal.create_http_client(client_options.merge( default_token_params: { client_id: 'bob' }, use_token_auth: true, key: api_key @@ -99,7 +99,7 @@ def encode64(text) end context 'with an :auth_callback lambda (clientId provided in library options instead of as a token_request param)' do - let(:client) { Ably::Internal.create_rest_client(client_options.merge(client_id: client_id, auth_callback: lambda { |token_params| token_request })) } + let(:client) { Ably::Internal.create_http_client(client_options.merge(client_id: client_id, auth_callback: lambda { |token_params| token_request })) } let(:token_request) { client.auth.create_token_request({}, key_name: key_name, key_secret: key_secret) } it 'correctly sets the clientId on the token' do @@ -139,7 +139,7 @@ def encode64(text) let(:client_options) { default_options.merge(key: api_key, client_id: client_id) } let!(:get_message_history_stub) do - stub_request(:get, "https://#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}") + stub_request(:get, "https://#{environment}-#{Ably::PubSub::Http::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}") .with(headers: { 'X-Ably-ClientId' => encode64(client_id) }) .to_return(body: [], headers: { 'Content-Type' => 'application/json' }) end @@ -155,7 +155,7 @@ def encode64(text) let(:client_options) { default_options.merge(token: token_string) } let!(:get_message_history_stub) do - stub_request(:get, "#{http_protocol}://#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}"). + stub_request(:get, "#{http_protocol}://#{environment}-#{Ably::PubSub::Http::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}"). with(headers: { 'Authorization' => "Bearer #{encode64(token_string)}" }). to_return(body: [], headers: { 'Content-Type' => 'application/json' }) end @@ -184,7 +184,7 @@ def encode64(text) context 'using tokens' do let(:client) do - Ably::Internal.create_rest_client(client_options.merge(auth_callback: lambda do |token_params| + Ably::Internal.create_http_client(client_options.merge(auth_callback: lambda do |token_params| @request_index ||= 0 @request_index += 1 send("token_request_#{@request_index > 2 ? 'next' : @request_index}") @@ -314,7 +314,7 @@ def encode64(text) context 'and custom fallback hosts are empty' do let(:client_options) { default_options.merge(environment: 'sandbox', key: api_key, fallback_hosts: []) } let!(:default_host_request_stub) do - stub_request(:post, "https://#{environment}-#{Ably::Rest::Client::DOMAIN}#{path}").to_return do + stub_request(:post, "https://#{environment}-#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return do raise Faraday::TimeoutError.new('timeout error message') end end @@ -366,7 +366,7 @@ def encode64(text) context 'and connection times out' do let!(:default_host_request_stub) do - stub_request(:post, "https://#{Ably::Rest::Client::DOMAIN}#{path}").to_return do + stub_request(:post, "https://#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return do raise Faraday::TimeoutError.new('timeout error message') end end @@ -380,7 +380,7 @@ def encode64(text) context "and the total request time exeeds #{http_defaults.fetch(:max_retry_duration)} seconds" do let!(:default_host_request_stub) do - stub_request(:post, "https://#{Ably::Rest::Client::DOMAIN}#{path}").to_return do + stub_request(:post, "https://#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return do sleep max_retry_duration * 1.5 raise Faraday::TimeoutError.new('timeout error message') end @@ -397,7 +397,7 @@ def encode64(text) context 'and connection fails' do let!(:default_host_request_stub) do - stub_request(:post, "https://#{Ably::Rest::Client::DOMAIN}#{path}").to_return do + stub_request(:post, "https://#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return do raise Faraday::ConnectionFailed.new('connection failure error message') end end @@ -422,7 +422,7 @@ def encode64(text) end let(:requests) { [] } let!(:default_host_request_stub) do - stub_request(:post, "https://#{Ably::Rest::Client::DOMAIN}#{path}").to_return do + stub_request(:post, "https://#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return do requests << true if requests.count == 1 raise Faraday::ConnectionFailed.new('connection failure error message') @@ -450,7 +450,7 @@ def encode64(text) context 'and basic authentication fails' do let(:status) { 401 } let!(:default_host_request_stub) do - stub_request(:post, "https://#{Ably::Rest::Client::DOMAIN}#{path}").to_return( + stub_request(:post, "https://#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return( headers: { 'Content-Type' => 'application/json' }, status: status, body: { @@ -482,7 +482,7 @@ def encode64(text) end end let!(:default_host_request_stub) do - stub_request(:post, "https://#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block) + stub_request(:post, "https://#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return(&fallback_block) end it 'attempts the fallback hosts as this is an authentication failure (#RSC15d)' do @@ -518,7 +518,7 @@ def encode64(text) end end let!(:default_host_request_stub) do - stub_request(:post, "https://#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block) + stub_request(:post, "https://#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return(&fallback_block) end context 'with custom fallback hosts provided' do @@ -863,7 +863,7 @@ def encode64(text) end end let!(:default_host_request_stub) do - stub_request(:post, "https://#{env}-#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block) + stub_request(:post, "https://#{env}-#{Ably::PubSub::Http::Client::DOMAIN}#{path}").to_return(&fallback_block) end context 'with no fallback hosts provided (#TBC, see https://github.com/ably/wiki/issues/361)' do @@ -1168,7 +1168,7 @@ def encode64(text) it 'raises an exception once body size in bytes exceeded' do expect { - client.request(:post, endpoint, {}, { content: 'x' * Ably::Rest::Client::MAX_FRAME_SIZE }) + client.request(:post, endpoint, {}, { content: 'x' * Ably::PubSub::Http::Client::MAX_FRAME_SIZE }) }.to raise_error(Ably::Exceptions::MaxFrameSizeExceeded) end end @@ -1203,7 +1203,7 @@ def encode64(text) it 'raises an exception once body size in bytes exceeded' do expect { - client.request(:patch, endpoint, {}, { content: 'x' * Ably::Rest::Client::MAX_FRAME_SIZE }) + client.request(:patch, endpoint, {}, { content: 'x' * Ably::PubSub::Http::Client::MAX_FRAME_SIZE }) }.to raise_error(Ably::Exceptions::MaxFrameSizeExceeded) end end @@ -1232,7 +1232,7 @@ def encode64(text) it 'raises an exception once body size in bytes exceeded' do expect { - client.request(:put, endpoint, {}, { content: 'x' * Ably::Rest::Client::MAX_FRAME_SIZE }) + client.request(:put, endpoint, {}, { content: 'x' * Ably::PubSub::Http::Client::MAX_FRAME_SIZE }) }.to raise_error(Ably::Exceptions::MaxFrameSizeExceeded) end end @@ -1349,8 +1349,8 @@ def encode64(text) let(:token_params) { { nonce: "samenonce_#{protocol}", timestamp: Time.now.to_i } } it 'includes request_id in UnauthorizedRequest error due to replayed nonce' do - client1 = Ably::Internal.create_rest_client(default_options.merge(key: api_key)) - client2 = Ably::Internal.create_rest_client(default_options.merge(key: api_key, add_request_ids: true)) + client1 = Ably::Internal.create_http_client(default_options.merge(key: api_key)) + client2 = Ably::Internal.create_http_client(default_options.merge(key: api_key, add_request_ids: true)) expect { client1.auth.request_token(token_params) }.not_to raise_error begin client2.auth.request_token(token_params) @@ -1374,7 +1374,7 @@ def encode64(text) let(:client_options) do default_options.merge( rest_host: 'non.existent.domain.local', - fallback_hosts: [[environment, Ably::Rest::Client::DOMAIN].join('-')], + fallback_hosts: [[environment, Ably::PubSub::Http::Client::DOMAIN].join('-')], key: api_key, logger: custom_logger, log_retries_as_info: false) diff --git a/spec/acceptance/rest/encoders_spec.rb b/spec/acceptance/http/encoders_spec.rb similarity index 98% rename from spec/acceptance/rest/encoders_spec.rb rename to spec/acceptance/http/encoders_spec.rb index 36a35a771..830cf7765 100644 --- a/spec/acceptance/rest/encoders_spec.rb +++ b/spec/acceptance/http/encoders_spec.rb @@ -4,7 +4,7 @@ describe Ably::Models::MessageEncoders do let(:default_client_options) { { key: api_key, environment: environment } } - let(:client) { Ably::Internal.create_rest_client(default_client_options.merge(protocol: protocol)) } + let(:client) { Ably::Internal.create_http_client(default_client_options.merge(protocol: protocol)) } let(:channel_options) { {} } let(:channel) { client.channel('test', channel_options) } let(:response) { instance_double('Faraday::Response', status: 201) } diff --git a/spec/acceptance/rest/message_spec.rb b/spec/acceptance/http/message_spec.rb similarity index 97% rename from spec/acceptance/rest/message_spec.rb rename to spec/acceptance/http/message_spec.rb index 550066896..1d9c87492 100644 --- a/spec/acceptance/rest/message_spec.rb +++ b/spec/acceptance/http/message_spec.rb @@ -3,14 +3,14 @@ require 'base64' require 'securerandom' -describe Ably::Rest::Channel, 'messages' do +describe Ably::PubSub::Http::Channel, 'messages' do include Ably::Modules::Conversions vary_by_protocol do let(:default_client_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_client_options } - let(:client) { Ably::Internal.create_rest_client(client_options) } - let(:other_client) { Ably::Internal.create_rest_client(client_options) } + let(:client) { Ably::Internal.create_http_client(client_options) } + let(:other_client) { Ably::Internal.create_http_client(client_options) } let(:channel) { client.channel(random_str) } context 'publishing with an ASCII_8BIT message name' do @@ -206,16 +206,16 @@ specify 'idempotent publishing is set as per clientOptions' do # set idempotent_rest_publishing to false - client = Ably::Internal.create_rest_client(key: api_key, protocol: protocol, idempotent_rest_publishing: false) + client = Ably::Internal.create_http_client(key: api_key, protocol: protocol, idempotent_rest_publishing: false) expect(client.idempotent_rest_publishing).to be_falsey # set idempotent_rest_publishing to true - client = Ably::Internal.create_rest_client(key: api_key, protocol: protocol, idempotent_rest_publishing: true) + client = Ably::Internal.create_http_client(key: api_key, protocol: protocol, idempotent_rest_publishing: true) expect(client.idempotent_rest_publishing).to be_truthy end specify 'idempotent publishing is enabled by default (#TO3n)' do - client = Ably::Internal.create_rest_client(key: api_key, protocol: protocol) + client = Ably::Internal.create_http_client(key: api_key, protocol: protocol) expect(client.idempotent_rest_publishing).to be_truthy end @@ -436,7 +436,7 @@ def self.add_tests_for_data(data) context 'when retrieving #history with a different protocol' do let(:other_protocol) { protocol == :msgpack ? :json : :msgpack } - let(:other_client) { Ably::Internal.create_rest_client(default_client_options.merge(protocol: other_protocol)) } + let(:other_client) { Ably::Internal.create_http_client(default_client_options.merge(protocol: other_protocol)) } let(:other_client_channel) { other_client.channel(channel_name, cipher: cipher_options) } before do diff --git a/spec/acceptance/rest/presence_spec.rb b/spec/acceptance/http/presence_spec.rb similarity index 98% rename from spec/acceptance/rest/presence_spec.rb rename to spec/acceptance/http/presence_spec.rb index 7386b39b3..b4465be79 100644 --- a/spec/acceptance/rest/presence_spec.rb +++ b/spec/acceptance/http/presence_spec.rb @@ -1,14 +1,14 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest::Presence do +describe Ably::PubSub::Http::Presence do include Ably::Modules::Conversions vary_by_protocol do let(:default_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_options } let(:client) do - Ably::Internal.create_rest_client(client_options) + Ably::Internal.create_http_client(client_options) end let(:fixtures) do @@ -228,7 +228,7 @@ def stub_presence_get(query) client.endpoint end let(:client) do - Ably::Internal.create_rest_client(key: "#{user}:#{secret}") + Ably::Internal.create_http_client(key: "#{user}:#{secret}") end let(:history_options) do { @@ -353,7 +353,7 @@ def message(client_id, messages) client.endpoint end let(:client) do - Ably::Internal.create_rest_client(client_options.merge(key: "#{user}:#{secret}")) + Ably::Internal.create_http_client(client_options.merge(key: "#{user}:#{secret}")) end let(:data) { random_str(32) } diff --git a/spec/acceptance/rest/push_admin_spec.rb b/spec/acceptance/http/push_admin_spec.rb similarity index 99% rename from spec/acceptance/rest/push_admin_spec.rb rename to spec/acceptance/http/push_admin_spec.rb index 4b042dcef..a5f36fbd2 100644 --- a/spec/acceptance/rest/push_admin_spec.rb +++ b/spec/acceptance/http/push_admin_spec.rb @@ -1,14 +1,14 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest::Push::Admin do +describe Ably::PubSub::Http::Push::Admin do include Ably::Modules::Conversions vary_by_protocol do let(:default_options) { { key: api_key, environment: environment, protocol: protocol} } let(:client_options) { default_options } let(:client) do - Ably::Internal.create_rest_client(client_options) + Ably::Internal.create_http_client(client_options) end let(:basic_notification_payload) do diff --git a/spec/acceptance/rest/stats_spec.rb b/spec/acceptance/http/stats_spec.rb similarity index 98% rename from spec/acceptance/rest/stats_spec.rb rename to spec/acceptance/http/stats_spec.rb index a1cb46136..9d545353f 100644 --- a/spec/acceptance/rest/stats_spec.rb +++ b/spec/acceptance/http/stats_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest::Client, '#stats' do +describe Ably::PubSub::Http::Client, '#stats' do include Ably::Modules::Conversions LAST_YEAR = Time.now.year - 1 @@ -48,7 +48,7 @@ end vary_by_protocol do - let(:client) { Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) } + let(:client) { Ably::Internal.create_http_client(key: api_key, environment: environment, protocol: protocol) } describe 'fetching application stats' do it 'returns a PaginatedResult object' do diff --git a/spec/acceptance/rest/time_spec.rb b/spec/acceptance/http/time_spec.rb similarity index 78% rename from spec/acceptance/rest/time_spec.rb rename to spec/acceptance/http/time_spec.rb index fd1c9a55d..f64f6b32f 100644 --- a/spec/acceptance/rest/time_spec.rb +++ b/spec/acceptance/http/time_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Ably::Rest::Client, '#time' do +describe Ably::PubSub::Http::Client, '#time' do vary_by_protocol do let(:client) do - Ably::Internal.create_rest_client(key: api_key, environment: environment, protocol: protocol) + Ably::Internal.create_http_client(key: api_key, environment: environment, protocol: protocol) end describe 'fetching the service time' do @@ -13,7 +13,7 @@ context 'with reconfigured HTTP timeout' do let(:client) do - Ably::Internal.create_rest_client(http_request_timeout: 0.0001, key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) + Ably::Internal.create_http_client(http_request_timeout: 0.0001, key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true) end it 'should raise a timeout exception' do diff --git a/spec/acceptance/realtime/auth_spec.rb b/spec/acceptance/realtime/auth_spec.rb index 580bfac90..0b4d39175 100644 --- a/spec/acceptance/realtime/auth_spec.rb +++ b/spec/acceptance/realtime/auth_spec.rb @@ -4,7 +4,7 @@ # Very high level test coverage of the Realtime::Auth object which is just an async # wrapper around the Ably::Auth object # -describe Ably::Realtime::Auth, :event_machine do +describe Ably::PubSub::Realtime::Auth, :event_machine do def disconnect_transport(connection) if connection.transport connection.transport.close_connection_after_writing @@ -200,7 +200,7 @@ def disconnect_transport(connection) end context 'with auth_callback blocking' do - let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } + let(:rest_auth_client) { Ably::Internal.create_http_client(default_options.merge(key: api_key)) } let(:client_options) { default_options.merge(auth_callback: auth_callback) } let(:pause) { 5 } @@ -231,7 +231,7 @@ def disconnect_transport(connection) context 'when implicitly called, with an explicit ClientOptions client_id' do let(:client_id) { random_str } let(:client_options) { default_options.merge(auth_callback: lambda { |token_params| auth_token_object }, client_id: client_id, log_level: :none) } - let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: 'invalid')) } + let(:rest_auth_client) { Ably::Internal.create_http_client(default_options.merge(key: api_key, client_id: 'invalid')) } context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do let(:auth_token_object) { rest_auth_client.auth.request_token } @@ -280,8 +280,8 @@ def disconnect_transport(connection) let(:client_id) { random_str } let(:client_options) { default_options.merge(auth_callback: auth_proc, client_id: client_id, log_level: :none) } - let(:valid_auth_token) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: client_id)).auth.request_token } - let(:invalid_auth_token) { Ably::Internal.create_rest_client(default_options.merge(key: api_key, client_id: 'invalid')).auth.request_token } + let(:valid_auth_token) { Ably::Internal.create_http_client(default_options.merge(key: api_key, client_id: client_id)).auth.request_token } + let(:invalid_auth_token) { Ably::Internal.create_http_client(default_options.merge(key: api_key, client_id: 'invalid')).auth.request_token } context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do it 'rejects a TokenDetails object with an incompatible client_id and fails with an exception' do @@ -301,7 +301,7 @@ def disconnect_transport(connection) end context 'when already authenticated with a valid token' do - let(:rest_client) { Ably::Internal.create_rest_client(default_options) } + let(:rest_client) { Ably::Internal.create_http_client(default_options) } let(:client_publisher) { auto_close Ably::Internal.create_realtime_client(default_options) } let(:basic_capability) { JSON.dump("foo" => ["subscribe"]) } let(:basic_token_cb) { lambda do |token_params| @@ -833,7 +833,7 @@ def disconnect_transport(connection) end describe '#client_id_validated?' do - let(:auth) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)).auth } + let(:auth) { Ably::Internal.create_http_client(default_options.merge(key: api_key)).auth } context 'when using basic auth' do let(:client_options) { default_options.merge(key: api_key) } @@ -1082,7 +1082,7 @@ def disconnect_transport(connection) context 'when using auth_callback' do let(:token_callback) do lambda do |token_params| - Ably::Internal.create_rest_client(default_options).auth.request_token({}, { auth_url: auth_url, auth_params: auth_params }).token + Ably::Internal.create_http_client(default_options).auth.request_token({}, { auth_url: auth_url, auth_params: auth_params }).token end end let(:client_options) { default_options.merge(auth_callback: token_callback) } diff --git a/spec/acceptance/realtime/channel_history_spec.rb b/spec/acceptance/realtime/channel_history_spec.rb index 99ed28ec9..f88e040eb 100644 --- a/spec/acceptance/realtime/channel_history_spec.rb +++ b/spec/acceptance/realtime/channel_history_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Channel, '#history', :event_machine do +describe Ably::PubSub::Realtime::Channel, '#history', :event_machine do vary_by_protocol do let(:default_options) { options.merge(key: api_key, environment: environment, protocol: protocol) } diff --git a/spec/acceptance/realtime/channel_spec.rb b/spec/acceptance/realtime/channel_spec.rb index e0e766739..da3e80f8b 100644 --- a/spec/acceptance/realtime/channel_spec.rb +++ b/spec/acceptance/realtime/channel_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Channel, :event_machine do +describe Ably::PubSub::Realtime::Channel, :event_machine do vary_by_protocol do let(:default_options) { { key: api_key, environment: environment, protocol: protocol } } let(:client_options) { default_options } @@ -244,7 +244,7 @@ def disconnect_transport it 'calls the SafeDeferrable callback on success (#RTL4d)' do channel.attach.callback do - expect(channel).to be_a(Ably::Realtime::Channel) + expect(channel).to be_a(Ably::PubSub::Realtime::Channel) expect(channel.state).to eq(:attached) stop_reactor end @@ -583,7 +583,7 @@ def disconnect_transport it 'calls the Deferrable callback on success' do channel.once :attached do channel.detach.callback do - expect(channel).to be_a(Ably::Realtime::Channel) + expect(channel).to be_a(Ably::PubSub::Realtime::Channel) expect(channel.state).to eq(:detached) stop_reactor end @@ -1377,7 +1377,7 @@ def disconnect_transport end context 'with two invalid message out of 12' do - let(:rest_client) { Ably::Internal.create_rest_client(default_options.merge(client_id: 'valid')) } + let(:rest_client) { Ably::Internal.create_http_client(default_options.merge(client_id: 'valid')) } let(:invalid_messages) do 2.times.map do |index| @@ -1416,7 +1416,7 @@ def disconnect_transport end context 'only invalid messages' do - let(:rest_client) { Ably::Internal.create_rest_client(default_options.merge(client_id: 'valid')) } + let(:rest_client) { Ably::Internal.create_http_client(default_options.merge(client_id: 'valid')) } let(:invalid_messages) do 10.times.map do |index| @@ -1484,7 +1484,7 @@ def disconnect_transport 65536 it 'rejects the publish' do - messages = (Ably::Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE + 1).times.map do + messages = (Ably::PubSub::Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE + 1).times.map do { name: 'foo' } end @@ -1497,7 +1497,7 @@ def disconnect_transport context 'identified clients' do context 'when authenticated with a wildcard client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: '*') } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: '*') } let(:client_options) { default_options.merge(key: nil, token: token) } let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } @@ -1546,7 +1546,7 @@ def disconnect_transport end context 'when authenticated with a Token string with an implicit client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid').token } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: 'valid').token } let(:client_options) { default_options.merge(key: nil, token: token) } let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } @@ -1637,7 +1637,7 @@ def disconnect_transport end context 'when authenticated with a valid client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid') } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: 'valid') } let(:client_options) { default_options.merge(key: nil, token: token) } let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } @@ -1686,7 +1686,7 @@ def disconnect_transport end context 'when anonymous and no client_id' do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: nil) } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: nil) } let(:client_options) { default_options.merge(key: nil, token: token) } let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:channel) { client.channels.get(channel_name) } @@ -2414,8 +2414,8 @@ def fake_error(error) end describe '#presence' do - it 'returns a Ably::Realtime::Presence object' do - expect(channel.presence).to be_a(Ably::Realtime::Presence) + it 'returns a Ably::PubSub::Realtime::Presence object' do + expect(channel.presence).to be_a(Ably::PubSub::Realtime::Presence) stop_reactor end end @@ -2491,7 +2491,7 @@ def self.build_flags(flags) context 'ChannelStateChange object' do it 'has current state' do channel.on(:attached) do |channel_state_change| - expect(channel_state_change.current).to be_a(Ably::Realtime::Channel::STATE) + expect(channel_state_change.current).to be_a(Ably::PubSub::Realtime::Channel::STATE) expect(channel_state_change.current).to eq(:attached) stop_reactor end @@ -2500,7 +2500,7 @@ def self.build_flags(flags) it 'has a previous state' do channel.on(:attached) do |channel_state_change| - expect(channel_state_change.previous).to be_a(Ably::Realtime::Channel::STATE) + expect(channel_state_change.previous).to be_a(Ably::PubSub::Realtime::Channel::STATE) expect(channel_state_change.previous).to eq(:attaching) stop_reactor end @@ -2509,7 +2509,7 @@ def self.build_flags(flags) it 'has the event that generated the state change (#TA5)' do channel.on(:attached) do |channel_state_change| - expect(channel_state_change.event).to be_a(Ably::Realtime::Channel::EVENT) + expect(channel_state_change.event).to be_a(Ably::PubSub::Realtime::Channel::EVENT) expect(channel_state_change.event).to eq(:attached) stop_reactor end diff --git a/spec/acceptance/realtime/channels_spec.rb b/spec/acceptance/realtime/channels_spec.rb index e8958f0be..582f6f297 100644 --- a/spec/acceptance/realtime/channels_spec.rb +++ b/spec/acceptance/realtime/channels_spec.rb @@ -1,10 +1,10 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Channels, :event_machine do +describe Ably::PubSub::Realtime::Channels, :event_machine do shared_examples 'a channel' do it 'returns a channel object' do - expect(channel).to be_a Ably::Realtime::Channel + expect(channel).to be_a Ably::PubSub::Realtime::Channel expect(channel.name).to eql(channel_name) stop_reactor end @@ -82,7 +82,7 @@ it 'overrides the existing channel options and returns the channel object' do expect(original_channel.options.to_h).to_not include(:encrypted) new_channel = client.channels.get(channel_name, new_channel_options) - expect(new_channel).to be_a(Ably::Realtime::Channel) + expect(new_channel).to be_a(Ably::PubSub::Realtime::Channel) expect(new_channel.options[:encrypted]).to eql(true) stop_reactor end @@ -105,7 +105,7 @@ it 'returns the existing channel without modifying the channel options' do expect(original_channel.options.to_h).to eq(options) new_channel = client.channels.get(channel_name) - expect(new_channel).to be_a(Ably::Realtime::Channel) + expect(new_channel).to be_a(Ably::PubSub::Realtime::Channel) expect(original_channel.options.to_h).to eq(options) stop_reactor end diff --git a/spec/acceptance/realtime/client_spec.rb b/spec/acceptance/realtime/client_spec.rb index cdef09a9a..a9ddd3edc 100644 --- a/spec/acceptance/realtime/client_spec.rb +++ b/spec/acceptance/realtime/client_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Client, :event_machine do +describe Ably::PubSub::Realtime::Client, :event_machine do vary_by_protocol do let(:default_options) do { key: api_key, environment: environment, protocol: protocol } @@ -136,7 +136,7 @@ context 'with a wildcard client_id token ' do subject { auto_close Ably::Internal.create_realtime_client(client_options) } let(:client_options) { default_options.merge(auth_callback: lambda { |token_params| auth_token_object }, client_id: client_id) } - let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } + let(:rest_auth_client) { Ably::Internal.create_http_client(default_options.merge(key: api_key)) } let(:auth_token_object) { rest_auth_client.auth.request_token(client_id: '*') } context 'and an explicit client_id in ClientOptions' do @@ -212,21 +212,21 @@ context '#connection' do it 'provides access to the Connection object' do - expect(subject.connection).to be_a(Ably::Realtime::Connection) + expect(subject.connection).to be_a(Ably::PubSub::Realtime::Connection) stop_reactor end end context '#channels' do it 'provides access to the Channels collection object' do - expect(subject.channels).to be_a(Ably::Realtime::Channels) + expect(subject.channels).to be_a(Ably::PubSub::Realtime::Channels) stop_reactor end end context '#auth' do it 'provides access to the Realtime::Auth object' do - expect(subject.auth).to be_a(Ably::Realtime::Auth) + expect(subject.auth).to be_a(Ably::PubSub::Realtime::Auth) stop_reactor end end @@ -479,7 +479,7 @@ let(:channel_name) { random_str } it 'rejects the publish' do - messages = (Ably::Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE + 1).times.map do + messages = (Ably::PubSub::Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE + 1).times.map do { name: 'foo' } end diff --git a/spec/acceptance/realtime/connection_failures_spec.rb b/spec/acceptance/realtime/connection_failures_spec.rb index 560147665..989090e9c 100644 --- a/spec/acceptance/realtime/connection_failures_spec.rb +++ b/spec/acceptance/realtime/connection_failures_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Connection, 'failures', :event_machine do +describe Ably::PubSub::Realtime::Connection, 'failures', :event_machine do let(:connection) { client.connection } vary_by_protocol do @@ -14,7 +14,7 @@ auto_close Ably::Internal.create_realtime_client(client_options) end let(:rest_client) do - Ably::Internal.create_rest_client(default_options) + Ably::Internal.create_http_client(default_options) end context 'authentication failure' do @@ -111,7 +111,7 @@ end end - context 'request fails due to slow response and subsequent timeout', :webmock, em_timeout: (Ably::Rest::Client::HTTP_DEFAULTS.fetch(:request_timeout) + 5) * 2 do + context 'request fails due to slow response and subsequent timeout', :webmock, em_timeout: (Ably::PubSub::Http::Client::HTTP_DEFAULTS.fetch(:request_timeout) + 5) * 2 do let(:auth_url) { "http://#{random_str}.domain.will.be.stubbed/path" } let(:client_options) { default_options.reject { |k, v| k == :key }.merge(auth_url: auth_url, log_level: :fatal) } @@ -119,7 +119,7 @@ before do stub_request(:get, auth_url). to_return do |request| - sleep Ably::Rest::Client::HTTP_DEFAULTS.fetch(:request_timeout) + 5 + sleep Ably::PubSub::Http::Client::HTTP_DEFAULTS.fetch(:request_timeout) + 5 { status: [500, "Internal Server Error"] } end end @@ -151,12 +151,12 @@ # Timeout +5 seconds, beyond default allowed timeout before do - token_response = Ably::Internal.create_rest_client(default_options).auth.request_token + token_response = Ably::Internal.create_http_client(default_options).auth.request_token WebMock.enable! stub_request(:get, auth_url). to_return do |request| - sleep Ably::Rest::Client::HTTP_DEFAULTS.fetch(:request_timeout) + sleep Ably::PubSub::Http::Client::HTTP_DEFAULTS.fetch(:request_timeout) { status: [500, "Internal Server Error"] } end.then. to_return(:status => 201, :body => token_response.to_json, :headers => { 'Content-Type' => 'application/json' }) @@ -860,7 +860,7 @@ def send_disconnect_message ) end - it "retries every #{Ably::Realtime::Connection::DEFAULTS.fetch(:disconnected_retry_timeout)} seconds" do + it "retries every #{Ably::PubSub::Realtime::Connection::DEFAULTS.fetch(:disconnected_retry_timeout)} seconds" do fail_if_suspended_or_failed stubbed_first_attempt = false @@ -930,7 +930,7 @@ def send_disconnect_message original_method.call(*args, &block) end connection.once(:connected) do - host = "#{"#{environment}-" if environment && environment.to_s != 'production'}#{Ably::Realtime::Client::DOMAIN}" + host = "#{"#{environment}-" if environment && environment.to_s != 'production'}#{Ably::PubSub::Realtime::Client::DOMAIN}" expect(hosts.first).to eql(host) expect(hosts.length).to eql(1) stop_reactor @@ -969,7 +969,7 @@ def send_disconnect_message end # Create a new message dispatcher that subscribes to ProtocolMessages after the previous subscription allowing us # to modify the ProtocolMessage - Ably::Realtime::Client::IncomingMessageDispatcher.new(client, connection) + Ably::PubSub::Realtime::Client::IncomingMessageDispatcher.new(client, connection) end connection.once(:connected) do |connection_state_change| @@ -1470,7 +1470,7 @@ def kill_connection_transport_and_prevent_valid_resume context 'with non-production environment' do let(:environment) { 'sandbox' } - let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" } + let(:expected_host) { "#{environment}-#{Ably::PubSub::Realtime::Client::DOMAIN}" } let(:client_options) { timeout_options.merge(environment: environment) } context ':fallback_hosts_use_default is unset' do @@ -1568,7 +1568,7 @@ def kill_connection_transport_and_prevent_valid_resume stub_const 'Ably::FALLBACK_HOSTS', custom_hosts end - let(:expected_host) { Ably::Realtime::Client::DOMAIN } + let(:expected_host) { Ably::PubSub::Realtime::Client::DOMAIN } let(:client_options) { timeout_options.merge(environment: nil) } let(:fallback_hosts_used) { Array.new } diff --git a/spec/acceptance/realtime/connection_spec.rb b/spec/acceptance/realtime/connection_spec.rb index 57794de2d..ad128862b 100644 --- a/spec/acceptance/realtime/connection_spec.rb +++ b/spec/acceptance/realtime/connection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'ostruct' -describe Ably::Realtime::Connection, :event_machine do +describe Ably::PubSub::Realtime::Connection, :event_machine do let(:connection) { client.connection } vary_by_protocol do @@ -129,7 +129,7 @@ let(:token_callback) do lambda do |token_params| auth_requests << Time.now - Ably::Internal.create_rest_client(default_options).auth.request_token(ttl: ttl).token + Ably::Internal.create_http_client(default_options).auth.request_token(ttl: ttl).token end end let(:client_options) { default_options.merge(auth_callback: token_callback, fallback_hosts: []) } @@ -245,7 +245,7 @@ lambda do |token_params| sleep 2 auth_requests << Time.now - Ably::Internal.create_rest_client(default_options).auth.request_token(ttl: ttl).token + Ably::Internal.create_http_client(default_options).auth.request_token(ttl: ttl).token end end let(:client_options) { default_options.merge(auth_callback: token_callback) } @@ -299,7 +299,7 @@ def publish_and_check_disconnect(options = {}) "#{app_id}.invalid-token-invalid-token-invalid-token" else @token_issued = true - Ably::Internal.create_rest_client(default_options).auth.request_token(ttl: ttl).token + Ably::Internal.create_http_client(default_options).auth.request_token(ttl: ttl).token end end end @@ -365,7 +365,7 @@ def publish_and_check_disconnect(options = {}) context 'with opaque token string that contain an implicit client_id' do let(:client_options) { default_options.merge(token: token_string, key: nil) } - let(:rest_auth_client) { Ably::Internal.create_rest_client(default_options.merge(key: api_key)) } + let(:rest_auth_client) { Ably::Internal.create_http_client(default_options.merge(key: api_key)) } let(:token_string) { rest_auth_client.auth.request_token(client_id: client_id).token } context 'string' do @@ -796,7 +796,7 @@ def expect_ordered_phases it 'calls the Deferrable callback on success' do connection.connect do connection.close.callback do - expect(connection).to be_a(Ably::Realtime::Connection) + expect(connection).to be_a(Ably::PubSub::Realtime::Connection) expect(connection.state).to eq(:closed) stop_reactor end @@ -1308,8 +1308,8 @@ def log_connection_changes context 'with a different default connection_state_ttl' do before do - old_defaults = Ably::Realtime::Connection::DEFAULTS - stub_const 'Ably::Realtime::Connection::DEFAULTS', old_defaults.merge(connection_state_ttl: 15) + old_defaults = Ably::PubSub::Realtime::Connection::DEFAULTS + stub_const 'Ably::PubSub::Realtime::Connection::DEFAULTS', old_defaults.merge(connection_state_ttl: 15) end it 'updates the private Connection#connection_state_ttl when received from Ably in ConnectionDetails' do @@ -1805,7 +1805,7 @@ def self.available_states context 'ConnectionStateChange object' do it 'has current state' do connection.on(:connected) do |connection_state_change| - expect(connection_state_change.current).to be_a(Ably::Realtime::Connection::STATE) + expect(connection_state_change.current).to be_a(Ably::PubSub::Realtime::Connection::STATE) expect(connection_state_change.current).to eq(:connected) stop_reactor end @@ -1813,7 +1813,7 @@ def self.available_states it 'has a previous state' do connection.on(:connected) do |connection_state_change| - expect(connection_state_change.previous).to be_a(Ably::Realtime::Connection::STATE) + expect(connection_state_change.previous).to be_a(Ably::PubSub::Realtime::Connection::STATE) expect(connection_state_change.previous).to eq(:connecting) stop_reactor end @@ -1821,7 +1821,7 @@ def self.available_states it 'has the event that generated the state change (#TH5)' do connection.on(:connected) do |connection_state_change| - expect(connection_state_change.event).to be_a(Ably::Realtime::Connection::EVENT) + expect(connection_state_change.event).to be_a(Ably::PubSub::Realtime::Connection::EVENT) expect(connection_state_change.event).to eq(:connected) stop_reactor end diff --git a/spec/acceptance/realtime/message_spec.rb b/spec/acceptance/realtime/message_spec.rb index 8220e3b64..51d1206ed 100644 --- a/spec/acceptance/realtime/message_spec.rb +++ b/spec/acceptance/realtime/message_spec.rb @@ -4,7 +4,7 @@ require 'json' require 'securerandom' -describe 'Ably::Realtime::Channel Message', :event_machine do +describe 'Ably::PubSub::Realtime::Channel Message', :event_machine do vary_by_protocol do let(:default_options) { options.merge(key: api_key, environment: environment, protocol: protocol) } let(:client_options) { default_options } @@ -298,7 +298,7 @@ def publish_and_check_extras(extras) let(:no_echo_channel) { no_echo_client.channel(channel_name) } let(:rest_client) do - Ably::Internal.create_rest_client(default_options) + Ably::Internal.create_http_client(default_options) end it 'will not echo messages to the client but will still broadcast messages to other connected clients', em_timeout: 10 do @@ -830,7 +830,7 @@ def publish_and_check_extras(extras) auto_close Ably::Internal.create_realtime_client(client_options) end let(:rest_client) do - Ably::Internal.create_rest_client(client_options) + Ably::Internal.create_http_client(client_options) end let(:realtime_channel) { realtime_client.channels.get(channel_name) } @@ -881,7 +881,7 @@ def publish_and_check_extras(extras) JSON.parse(File.read(fixtures_path))['messages'].each do |encoding_spec| context "when publishing a #{encoding_spec['expectedType']} using JSON protocol" do let(:rest_publish_client) do - Ably::Internal.create_rest_client(client_options.merge(protocol: :json)) + Ably::Internal.create_http_client(client_options.merge(protocol: :json)) end let(:realtime_subscribe_client) do Ably::Internal.create_realtime_client(client_options.merge(protocol: :msgpack)) @@ -909,10 +909,10 @@ def publish_and_check_extras(extras) context "when retrieving a #{encoding_spec['expectedType']} using JSON protocol" do let(:rest_publish_client) do - Ably::Internal.create_rest_client(client_options.merge(protocol: :msgpack)) + Ably::Internal.create_http_client(client_options.merge(protocol: :msgpack)) end let(:rest_retrieve_client) do - Ably::Internal.create_rest_client(client_options.merge(protocol: :json)) + Ably::Internal.create_http_client(client_options.merge(protocol: :json)) end let(:rest_publish_channel) { rest_publish_client.channels.get(channel_name) } diff --git a/spec/acceptance/realtime/presence_history_spec.rb b/spec/acceptance/realtime/presence_history_spec.rb index 0f6cbb51f..a6d27c6b3 100644 --- a/spec/acceptance/realtime/presence_history_spec.rb +++ b/spec/acceptance/realtime/presence_history_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Presence, 'history', :event_machine do +describe Ably::PubSub::Realtime::Presence, 'history', :event_machine do vary_by_protocol do let(:default_options) { { key: api_key, environment: environment, protocol: protocol } } diff --git a/spec/acceptance/realtime/presence_spec.rb b/spec/acceptance/realtime/presence_spec.rb index a7fbacbf5..e4e899866 100644 --- a/spec/acceptance/realtime/presence_spec.rb +++ b/spec/acceptance/realtime/presence_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Presence, :event_machine do +describe Ably::PubSub::Realtime::Presence, :event_machine do include Ably::Modules::Conversions vary_by_protocol do @@ -14,7 +14,7 @@ let(:client_two_id) { random_str } let(:client_two) { auto_close Ably::Internal.create_realtime_client(client_options.merge(client_id: client_two_id)) } - let(:wildcard_token) { lambda { |token_params| Ably::Internal.create_rest_client(client_options).auth.request_token(client_id: '*') } } + let(:wildcard_token) { lambda { |token_params| Ably::Internal.create_http_client(client_options).auth.request_token(client_id: '*') } } let(:channel_name) { "presence-#{random_str(4)}" } let(:channel_anonymous_client) { anonymous_client.channel(channel_name) } let(:presence_anonymous_client) { channel_anonymous_client.presence } @@ -353,7 +353,7 @@ def presence_action(method_name, data) shared_examples_for 'a presence on behalf of another client method' do |method_name| context ":#{method_name} when authenticated with a wildcard client_id" do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: '*').token } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: '*').token } let(:client_options) { default_options.merge(key: nil, token: token) } let(:client) { auto_close Ably::Internal.create_realtime_client(client_options) } let(:presence_channel) { client.channels.get(channel_name).presence } @@ -391,7 +391,7 @@ def presence_action(method_name, data) end context ":#{method_name} when authenticated with a valid client_id" do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: 'valid').token } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: 'valid').token } let(:client_options) { default_options.merge(key: nil, token: token) } let(:client) { auto_close Ably::Internal.create_realtime_client(client_options.merge(log_level: :error)) } let(:channel) { client.channels.get(channel_name) } @@ -433,7 +433,7 @@ def presence_action(method_name, data) end context ":#{method_name} when anonymous and no client_id" do - let(:token) { Ably::Internal.create_rest_client(default_options).auth.request_token(client_id: nil).token } + let(:token) { Ably::Internal.create_http_client(default_options).auth.request_token(client_id: nil).token } let(:client_options) { default_options.merge(key: nil, token: token) } let(:client) { auto_close Ably::Internal.create_realtime_client(client_options.merge(log_level: :error)) } let(:channel) { client.channels.get(channel_name) } @@ -2383,7 +2383,7 @@ def connect_members_deferrables let(:present_only_capability) do { channel_name => ["presence"] } end - let(:present_only_callback) { lambda { |token_params| Ably::Internal.create_rest_client(client_options).auth.request_token(client_id: '*', capability: present_only_capability) } } + let(:present_only_callback) { lambda { |token_params| Ably::Internal.create_http_client(client_options).auth.request_token(client_id: '*', capability: present_only_capability) } } let(:client_one) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: present_only_callback)) } it 'receives presence updates for all presence events generated by the current connection and the presence map is kept up to date (#RTP17a)' do @@ -2745,7 +2745,7 @@ def cripple_websocket_transport lambda do |token_params| # Pause to allow presence updates to occur whilst disconnected sleep 1 - Ably::Internal.create_rest_client(client_options).auth.request_token + Ably::Internal.create_http_client(client_options).auth.request_token end end let(:anonymous_client) { auto_close Ably::Internal.create_realtime_client(client_options.merge(auth_callback: auth_callback)) } diff --git a/spec/acceptance/realtime/push_admin_spec.rb b/spec/acceptance/realtime/push_admin_spec.rb index 8f2ceb84b..1933d2818 100644 --- a/spec/acceptance/realtime/push_admin_spec.rb +++ b/spec/acceptance/realtime/push_admin_spec.rb @@ -1,9 +1,9 @@ # encoding: utf-8 require 'spec_helper' -# These tests are a subset of Ably::Rest::Push::Admin in async EM style +# These tests are a subset of Ably::PubSub::Http::Push::Admin in async EM style # The more robust complete test suite is in rest/push_admin_spec.rb -describe Ably::Realtime::Push::Admin, :event_machine do +describe Ably::PubSub::Realtime::Push::Admin, :event_machine do include Ably::Modules::Conversions vary_by_protocol do diff --git a/spec/acceptance/realtime/stats_spec.rb b/spec/acceptance/realtime/stats_spec.rb index 84ca16dfb..7e8bb7feb 100644 --- a/spec/acceptance/realtime/stats_spec.rb +++ b/spec/acceptance/realtime/stats_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ably::Realtime::Client, '#stats', :event_machine do +describe Ably::PubSub::Realtime::Client, '#stats', :event_machine do vary_by_protocol do let(:client) do auto_close Ably::Internal.create_realtime_client(key: api_key, environment: environment, protocol: protocol) diff --git a/spec/acceptance/realtime/time_spec.rb b/spec/acceptance/realtime/time_spec.rb index 805befd88..106190bde 100644 --- a/spec/acceptance/realtime/time_spec.rb +++ b/spec/acceptance/realtime/time_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ably::Realtime::Client, '#time', :event_machine do +describe Ably::PubSub::Realtime::Client, '#time', :event_machine do vary_by_protocol do let(:client) do auto_close Ably::Internal.create_realtime_client(key: api_key, environment: environment, protocol: protocol) diff --git a/spec/shared/client_initializer_behaviour.rb b/spec/shared/client_initializer_behaviour.rb index 84ff2df4c..0db174762 100644 --- a/spec/shared/client_initializer_behaviour.rb +++ b/spec/shared/client_initializer_behaviour.rb @@ -18,7 +18,7 @@ def protocol end def rest? - subject.kind_of?(Ably::Rest::Client) + subject.kind_of?(Ably::PubSub::Http::Client) end context 'with invalid arguments' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e96e9cdf9..1b5c7a387 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -35,4 +35,4 @@ def console(message) # EM Helper must be loaded after rspec_config to ensure around block occurs before RSpec retry require 'support/event_machine_helper' -require 'support/rest_testapp_before_retry' +require 'support/http_testapp_before_retry' diff --git a/spec/support/rest_testapp_before_retry.rb b/spec/support/http_testapp_before_retry.rb similarity index 100% rename from spec/support/rest_testapp_before_retry.rb rename to spec/support/http_testapp_before_retry.rb diff --git a/spec/support/markdown_spec_formatter.rb b/spec/support/markdown_spec_formatter.rb index efc66a0ca..d79acb1cc 100644 --- a/spec/support/markdown_spec_formatter.rb +++ b/spec/support/markdown_spec_formatter.rb @@ -23,7 +23,7 @@ def initialize(output) def start(notification) puts "\n\e[33m --> Creating SPEC.md <--\e[0m\n" - scope = if defined?(Ably::Realtime) + scope = if defined?(Ably::PubSub::Realtime) 'Realtime & REST' else 'REST' diff --git a/spec/support/test_app.rb b/spec/support/test_app.rb index f0e4cfc93..6d79fc471 100644 --- a/spec/support/test_app.rb +++ b/spec/support/test_app.rb @@ -94,13 +94,13 @@ def realtime_host end def create_test_stats(stats) - client = Ably::Internal.create_rest_client(key: api_key, environment: environment) + client = Ably::Internal.create_http_client(key: api_key, environment: environment) response = client.post('/stats', stats) raise "Could not create stats fixtures. Ably responded with status #{response.status}\n#{response.body}" unless (200..299).include?(response.status) end private def sandbox_client - @sandbox_client ||= Ably::Internal.create_rest_client(key: 'app.key:secret', tls: true, environment: environment) + @sandbox_client ||= Ably::Internal.create_http_client(key: 'app.key:secret', tls: true, environment: environment) end end diff --git a/spec/unit/rest/channel_spec.rb b/spec/unit/http/channel_spec.rb similarity index 97% rename from spec/unit/rest/channel_spec.rb rename to spec/unit/http/channel_spec.rb index 39703be2e..928f98219 100644 --- a/spec/unit/rest/channel_spec.rb +++ b/spec/unit/http/channel_spec.rb @@ -1,10 +1,10 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest::Channel do +describe Ably::PubSub::Http::Channel do let(:client) do instance_double( - 'Ably::Rest::Client', + 'Ably::PubSub::Http::Client', encoders: [], post: instance_double('Faraday::Response', status: 201), idempotent_rest_publishing: false, max_message_size: max_message_size @@ -13,7 +13,7 @@ let(:channel_name) { 'unique' } let(:max_message_size) { nil } - subject { Ably::Rest::Channel.new(client, channel_name) } + subject { Ably::PubSub::Http::Channel.new(client, channel_name) } describe '#initializer' do let(:channel_name) { random_str.encode(encoding) } diff --git a/spec/unit/rest/channels_spec.rb b/spec/unit/http/channels_spec.rb similarity index 86% rename from spec/unit/rest/channels_spec.rb rename to spec/unit/http/channels_spec.rb index 5b4eba0dd..00753fee5 100644 --- a/spec/unit/rest/channels_spec.rb +++ b/spec/unit/http/channels_spec.rb @@ -1,20 +1,20 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest::Channels do - let(:client) { instance_double('Ably::Rest::Client', logger: double('logger').as_null_object) } +describe Ably::PubSub::Http::Channels do + let(:client) { instance_double('Ably::PubSub::Http::Client', logger: double('logger').as_null_object) } let(:channel_name) { 'unique'.encode(Encoding::UTF_8) } let(:options) do { params: { 'bizarre' => 'value' } } end - subject { Ably::Rest::Channels.new(client) } + subject { Ably::PubSub::Http::Channels.new(client) } describe '#get' do context "when channel doesn't exist" do shared_examples 'creates a channel' do it 'creates a channel (RSN3a)' do - expect(Ably::Rest::Channel).to receive(:new).with(client, channel_name, options) + expect(Ably::PubSub::Http::Channel).to receive(:new).with(client, channel_name, options) subject.get(channel_name, options) end end @@ -38,7 +38,7 @@ shared_examples 'reuse a channel object if it exists' do it 'will reuse a channel object if it exists (RSN3a)' do channel = subject.get(channel_name, channel_options) - expect(channel).to be_a(Ably::Rest::Channel) + expect(channel).to be_a(Ably::PubSub::Http::Channel) expect(subject.get(channel_name, channel_options).object_id).to eql(channel.object_id) end end @@ -89,7 +89,7 @@ end it '[] creates a channel' do - expect(Ably::Rest::Channel).to receive(:new).with(client, channel_name, options) + expect(Ably::PubSub::Http::Channel).to receive(:new).with(client, channel_name, options) subject.get(channel_name, options) end @@ -116,10 +116,10 @@ context 'is Enumerable' do let(:channel_count) { 5 } - let(:mock_channel) { instance_double('Ably::Rest::Channel') } + let(:mock_channel) { instance_double('Ably::PubSub::Http::Channel') } before do - allow(Ably::Rest::Channel).to receive(:new).and_return(mock_channel) + allow(Ably::PubSub::Http::Channel).to receive(:new).and_return(mock_channel) channel_count.times { |index| subject.get("channel-#{index}") } end diff --git a/spec/unit/rest/client_spec.rb b/spec/unit/http/client_spec.rb similarity index 90% rename from spec/unit/rest/client_spec.rb rename to spec/unit/http/client_spec.rb index 4ceb082d8..c15175203 100644 --- a/spec/unit/rest/client_spec.rb +++ b/spec/unit/http/client_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'shared/client_initializer_behaviour' -describe Ably::Rest::Client do +describe Ably::PubSub::Http::Client do subject do - Ably::Internal.create_rest_client(client_options) + Ably::Internal.create_http_client(client_options) end it_behaves_like 'a client initializer' @@ -93,7 +93,7 @@ let(:client_options) { { key: 'appid.keyuid:keysecret' } } it 'should return default 65536 (#TO3l8)' do - expect(subject.max_message_size).to eq(Ably::Rest::Client::MAX_MESSAGE_SIZE) + expect(subject.max_message_size).to eq(Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE) end end @@ -101,8 +101,8 @@ let(:client_options) { { key: 'appid.keyuid:keysecret', max_message_size: nil } } it 'should return default 65536 (#TO3l8)' do - expect(Ably::Rest::Client::MAX_MESSAGE_SIZE).to eq(65536) - expect(subject.max_message_size).to eq(Ably::Rest::Client::MAX_MESSAGE_SIZE) + expect(Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE).to eq(65536) + expect(subject.max_message_size).to eq(Ably::PubSub::Http::Client::MAX_MESSAGE_SIZE) end end @@ -131,7 +131,7 @@ end specify '#push returns a Push object' do - expect(subject.push).to be_a(Ably::Rest::Push) + expect(subject.push).to be_a(Ably::PubSub::Http::Push) end end end diff --git a/spec/unit/rest/rest_spec.rb b/spec/unit/http/http_spec.rb similarity index 65% rename from spec/unit/rest/rest_spec.rb rename to spec/unit/http/http_spec.rb index e53d59b35..6c6e96ff3 100644 --- a/spec/unit/rest/rest_spec.rb +++ b/spec/unit/http/http_spec.rb @@ -1,13 +1,13 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Rest do +describe Ably::PubSub::Http do let(:options) { { key: 'app.key:secret' } } - # This was a convenience alias for the Ably::Rest::Client constructor. It now refuses, + # This was a convenience alias for the Ably::PubSub::Http::Client constructor. It now refuses, # because a client's side is declared by the package it was created from. specify 'constructor refuses and points at the factory function' do - expect { Ably::Rest.new(options) } + expect { Ably::PubSub::Http.new(options) } .to raise_error(Ably::Exceptions::DirectConstructionNotSupported, /create_http_client/) end end diff --git a/spec/unit/rest/push_channel_spec.rb b/spec/unit/http/push_channel_spec.rb similarity index 70% rename from spec/unit/rest/push_channel_spec.rb rename to spec/unit/http/push_channel_spec.rb index e059a84b5..92dcf615d 100644 --- a/spec/unit/rest/push_channel_spec.rb +++ b/spec/unit/http/push_channel_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' -describe Ably::Rest::Channel::PushChannel do - subject { Ably::Rest::Channel::PushChannel } +describe Ably::PubSub::Http::Channel::PushChannel do + subject { Ably::PubSub::Http::Channel::PushChannel } let(:channel_name) { 'unique' } let(:client) { double('client').as_null_object } - let(:channel) { Ably::Rest::Channel.new(client, channel_name) } + let(:channel) { Ably::PubSub::Http::Channel.new(client, channel_name) } it 'is constructed with a channel' do - expect(subject.new(channel)).to be_a(Ably::Rest::Channel::PushChannel) + expect(subject.new(channel)).to be_a(Ably::PubSub::Http::Channel::PushChannel) end it 'raises an exception if constructed with an invalid type' do @@ -20,12 +20,12 @@ end it 'is available in the #push attribute of the channel' do - expect(channel.push).to be_a(Ably::Rest::Channel::PushChannel) + expect(channel.push).to be_a(Ably::PubSub::Http::Channel::PushChannel) expect(channel.push.channel).to eql(channel) end context 'methods not implemented as push notifications' do - subject { Ably::Rest::Channel::PushChannel.new(channel) } + subject { Ably::PubSub::Http::Channel::PushChannel.new(channel) } %w(subscribe_device subscribe_client_id unsubscribe_device unsubscribe_client_id get_subscriptions).each do |method_name| specify "##{method_name} raises an unsupported exception" do diff --git a/spec/unit/logger_spec.rb b/spec/unit/logger_spec.rb index 40ae656fc..1df2c5a91 100644 --- a/spec/unit/logger_spec.rb +++ b/spec/unit/logger_spec.rb @@ -2,7 +2,7 @@ describe Ably::Logger, :prevent_log_stubbing do let(:rest_client) do - instance_double('Ably::Rest::Client') + instance_double('Ably::PubSub::Http::Client') end subject { Ably::Logger.new(rest_client, Logger::INFO) } @@ -41,17 +41,17 @@ def uncolorize(string) end end - if defined?(Ably::Realtime) + if defined?(Ably::PubSub::Realtime) context 'with Realtime client' do let(:new_realtime_client) do - instance_double('Ably::Realtime::Client', connection: instance_double('Ably::Realtime::Connection', id: nil)) + instance_double('Ably::PubSub::Realtime::Client', connection: instance_double('Ably::PubSub::Realtime::Connection', id: nil)) end let(:connected_realtime_client) do - instance_double('Ably::Realtime::Client', connection: instance_double('Ably::Realtime::Connection', id: '0000')) + instance_double('Ably::PubSub::Realtime::Client', connection: instance_double('Ably::PubSub::Realtime::Connection', id: '0000')) end before do - allow(new_realtime_client).to receive(:kind_of?).with(Ably::Realtime::Client).and_return(true) - allow(connected_realtime_client).to receive(:kind_of?).with(Ably::Realtime::Client).and_return(true) + allow(new_realtime_client).to receive(:kind_of?).with(Ably::PubSub::Realtime::Client).and_return(true) + allow(connected_realtime_client).to receive(:kind_of?).with(Ably::PubSub::Realtime::Client).and_return(true) end context 'with Realtime disconnected client' do diff --git a/spec/unit/models/http_paginated_result_spec.rb b/spec/unit/models/http_paginated_result_spec.rb index 1c4ac1990..af29962b8 100644 --- a/spec/unit/models/http_paginated_result_spec.rb +++ b/spec/unit/models/http_paginated_result_spec.rb @@ -5,7 +5,7 @@ let(:paginated_result_class) { Ably::Models::HttpPaginatedResponse } let(:headers) { Hash.new } let(:client) do - instance_double('Ably::Rest::Client', logger: Ably::Models::NilLogger.new).tap do |client| + instance_double('Ably::PubSub::Http::Client', logger: Ably::Models::NilLogger.new).tap do |client| allow(client).to receive(:get).and_return(http_response) end end @@ -89,7 +89,7 @@ } end let(:paged_client) do - instance_double('Ably::Rest::Client', logger: Ably::Models::NilLogger.new).tap do |client| + instance_double('Ably::PubSub::Http::Client', logger: Ably::Models::NilLogger.new).tap do |client| allow(client).to receive(:get).and_return(http_response_page2) end end @@ -127,7 +127,7 @@ end end - if defined?(Ably::Realtime) + if defined?(Ably::PubSub::Realtime) context 'with option async_blocking_operations: true' do include RSpec::EventMachine diff --git a/spec/unit/models/message_encoders/base64_spec.rb b/spec/unit/models/message_encoders/base64_spec.rb index b941d1c7d..2aba0ee7a 100644 --- a/spec/unit/models/message_encoders/base64_spec.rb +++ b/spec/unit/models/message_encoders/base64_spec.rb @@ -9,7 +9,7 @@ let(:base64_data) { Base64.encode64(decoded_data) } let(:binary_data) { MessagePack.pack(decoded_data) } let(:base64_binary_data) { Base64.encode64(binary_data) } - let(:client) { instance_double('Ably::Realtime::Client') } + let(:client) { instance_double('Ably::PubSub::Realtime::Client') } subject { Ably::Models::MessageEncoders::Base64.new(client) } diff --git a/spec/unit/models/message_encoders/cipher_spec.rb b/spec/unit/models/message_encoders/cipher_spec.rb index 0c8e5f408..90a7a4e50 100644 --- a/spec/unit/models/message_encoders/cipher_spec.rb +++ b/spec/unit/models/message_encoders/cipher_spec.rb @@ -13,7 +13,7 @@ let(:binary_data) { MessagePack.pack(decoded_data) } let(:binary_cipher_data) { crypto.encrypt(binary_data) } - let(:client) { instance_double('Ably::Realtime::Client') } + let(:client) { instance_double('Ably::PubSub::Realtime::Client') } subject { Ably::Models::MessageEncoders::Cipher.new(client) } diff --git a/spec/unit/models/message_encoders/json_spec.rb b/spec/unit/models/message_encoders/json_spec.rb index 6fb6c9fe7..6d3cfe90d 100644 --- a/spec/unit/models/message_encoders/json_spec.rb +++ b/spec/unit/models/message_encoders/json_spec.rb @@ -9,7 +9,7 @@ let(:array_data) { ['value', 123] } let(:array_string_data) { JSON.dump(array_data) } - let(:client) { instance_double('Ably::Realtime::Client') } + let(:client) { instance_double('Ably::PubSub::Realtime::Client') } subject { Ably::Models::MessageEncoders::Json.new(client) } diff --git a/spec/unit/models/message_encoders/utf8_spec.rb b/spec/unit/models/message_encoders/utf8_spec.rb index d6d2c6b71..ec2024fd5 100644 --- a/spec/unit/models/message_encoders/utf8_spec.rb +++ b/spec/unit/models/message_encoders/utf8_spec.rb @@ -6,7 +6,7 @@ let(:string_ascii) { 'string'.encode(Encoding::ASCII_8BIT) } let(:string_utf8) { 'string'.encode(Encoding::UTF_8) } - let(:client) { instance_double('Ably::Realtime::Client') } + let(:client) { instance_double('Ably::PubSub::Realtime::Client') } subject { Ably::Models::MessageEncoders::Utf8.new(client) } diff --git a/spec/unit/models/paginated_result_spec.rb b/spec/unit/models/paginated_result_spec.rb index afd84c393..cc0cfd1dc 100644 --- a/spec/unit/models/paginated_result_spec.rb +++ b/spec/unit/models/paginated_result_spec.rb @@ -5,7 +5,7 @@ let(:paginated_result_class) { Ably::Models::PaginatedResult } let(:headers) { Hash.new } let(:client) do - instance_double('Ably::Rest::Client', logger: Ably::Models::NilLogger.new).tap do |client| + instance_double('Ably::PubSub::Http::Client', logger: Ably::Models::NilLogger.new).tap do |client| allow(client).to receive(:get).and_return(http_response) end end @@ -87,7 +87,7 @@ } end let(:paged_client) do - instance_double('Ably::Rest::Client', logger: Ably::Models::NilLogger.new).tap do |client| + instance_double('Ably::PubSub::Http::Client', logger: Ably::Models::NilLogger.new).tap do |client| allow(client).to receive(:get).and_return(http_response_page2) end end @@ -125,7 +125,7 @@ end end - if defined?(Ably::Realtime) + if defined?(Ably::PubSub::Realtime) context 'with option async_blocking_operations: true' do include RSpec::EventMachine diff --git a/spec/unit/pubsub/direct_construction_spec.rb b/spec/unit/pubsub/direct_construction_spec.rb index 945893fbd..1386040bb 100644 --- a/spec/unit/pubsub/direct_construction_spec.rb +++ b/spec/unit/pubsub/direct_construction_spec.rb @@ -6,14 +6,14 @@ # directly declares none and the platform cannot classify it for MAU billing. The classes # therefore refuse direct construction, and the per-side packages construct through # Ably::Internal instead. These specs guard both halves: the refusals must stay in place, -# and the internal entry points must keep producing usable clients. The Ably::Rest and -# Ably::Realtime module aliases refuse too, covered in their own module specs. +# and the internal entry points must keep producing usable clients. The Ably::PubSub::Http and +# Ably::PubSub::Realtime module aliases refuse too, covered in their own module specs. describe 'direct construction' do let(:api_key) { 'appid.keyuid:keysecret' } refused = { - 'Ably::Rest::Client.new' => -> (key) { Ably::Rest::Client.new(key) }, - 'Ably::Realtime::Client.new' => -> (key) { Ably::Realtime::Client.new(key) }, + 'Ably::PubSub::Http::Client.new' => -> (key) { Ably::PubSub::Http::Client.new(key) }, + 'Ably::PubSub::Realtime::Client.new' => -> (key) { Ably::PubSub::Realtime::Client.new(key) }, } refused.each do |subject_name, construct| @@ -41,26 +41,26 @@ describe 'Ably::Internal' do it 'creates a usable REST client' do - client = Ably::Internal.create_rest_client(key: api_key) - expect(client).to be_a(Ably::Rest::Client) + client = Ably::Internal.create_http_client(key: api_key) + expect(client).to be_a(Ably::PubSub::Http::Client) expect(client.auth.key).to eql(api_key) end it 'creates a usable realtime client' do client = Ably::Internal.create_realtime_client(key: api_key, auto_connect: false) - expect(client).to be_a(Ably::Realtime::Client) - expect(client.rest_client).to be_a(Ably::Rest::Client) + expect(client).to be_a(Ably::PubSub::Realtime::Client) + expect(client.rest_client).to be_a(Ably::PubSub::Http::Client) end # The realtime client builds its own REST client; if that call went back through the # public constructor, every realtime client would fail to build. it 'is what the realtime client uses to build its REST client' do - expect(Ably::Internal).to receive(:create_rest_client).once.and_call_original + expect(Ably::Internal).to receive(:create_http_client).once.and_call_original Ably::Internal.create_realtime_client(key: api_key, auto_connect: false) end it 'declares no side of its own' do - client = Ably::Internal.create_rest_client(key: api_key) + client = Ably::Internal.create_http_client(key: api_key) expect(client.agent).to_not include(Ably::PubSub::Server::SERVER_AGENT_IDENTIFIER) end end diff --git a/spec/unit/pubsub/packaging_spec.rb b/spec/unit/pubsub/packaging_spec.rb index 8a7d8e972..7e9bcc9cd 100644 --- a/spec/unit/pubsub/packaging_spec.rb +++ b/spec/unit/pubsub/packaging_spec.rb @@ -2,8 +2,10 @@ require 'spec_helper' # The Ably namespace is assembled at install time from two gems: ably-pubsub-core ships the -# implementation under lib/ably, and ably-pubsub-server ships only the lib/ably/pubsub/server -# subtree on top of it. That only holds together if each gem ships exactly its own subtree — +# implementation under lib/ably — including lib/ably/pubsub/http and lib/ably/pubsub/realtime — +# and ably-pubsub-server ships only the lib/ably/pubsub/server subtree on top of it. Both gems +# therefore write into lib/ably/pubsub, which makes the no-overlap rule below load-bearing +# rather than incidental. That only holds together if each gem ships exactly its own subtree — # a file shipped by both would be resolved from whichever gem comes first on the load path, # hiding the other's copy. The release pre-flight checks version agreement but nothing else # asserts the gems' file lists, so a packaging mistake would otherwise surface only after @@ -26,7 +28,15 @@ end it 'core does not ship the server subtree' do - expect(core_spec.files.grep(%r{\Alib/ably/pubsub(/|\.rb\z)})).to be_empty + expect(core_spec.files.grep(%r{\Alib/ably/pubsub/server(/|\.rb\z)})).to be_empty + end + + # Core owns everything under lib/ably/pubsub except the server subtree, so it must + # not claim the bare namespace paths the server gem would otherwise be free to add. + it 'core ships its own pubsub subtrees' do + expect(core_spec.files).to include('lib/ably/pubsub.rb') + expect(core_spec.files.grep(%r{\Alib/ably/pubsub/http/})).to_not be_empty + expect(core_spec.files.grep(%r{\Alib/ably/pubsub/realtime/})).to_not be_empty end it 'server ships only the lib/ably/pubsub/server subtree under lib' do diff --git a/spec/unit/pubsub/server_spec.rb b/spec/unit/pubsub/server_spec.rb index 9b2f20f2e..c114a64f0 100644 --- a/spec/unit/pubsub/server_spec.rb +++ b/spec/unit/pubsub/server_spec.rb @@ -23,7 +23,7 @@ subject(:client) { Ably::PubSub::Server.create_http_client(api_key) } it 'returns an HTTP (REST) client' do - expect(client).to be_a(Ably::Rest::Client) + expect(client).to be_a(Ably::PubSub::Http::Client) end it 'appends the side-declaring agent entry to the base agent, versionless' do @@ -82,11 +82,11 @@ subject(:client) { Ably::PubSub::Server.create_realtime_client(auto_connect: false, key: api_key) } it 'returns a realtime client' do - expect(client).to be_a(Ably::Realtime::Client) + expect(client).to be_a(Ably::PubSub::Realtime::Client) end it 'appends the side-declaring agent entry sent as the realtime agent connection param' do - # Ably::Realtime::Connection sends client.rest_client.agent as the `agent` param + # Ably::PubSub::Realtime::Connection sends client.rest_client.agent as the `agent` param expect(client.rest_client.agent).to eql("#{Ably::AGENT} #{side_entry}") end diff --git a/spec/unit/realtime/channel_spec.rb b/spec/unit/realtime/channel_spec.rb index 8fb596cde..ebfc17847 100644 --- a/spec/unit/realtime/channel_spec.rb +++ b/spec/unit/realtime/channel_spec.rb @@ -2,12 +2,12 @@ require 'spec_helper' require 'shared/protocol_msgbus_behaviour' -describe Ably::Realtime::Channel do +describe Ably::PubSub::Realtime::Channel do let(:client) { Ably::Internal.create_realtime_client(token: 'valid') } let(:channel_name) { 'test' } subject do - Ably::Realtime::Channel.new(client, channel_name) + Ably::PubSub::Realtime::Channel.new(client, channel_name) end describe '#initializer' do diff --git a/spec/unit/realtime/channels_spec.rb b/spec/unit/realtime/channels_spec.rb index a415a7144..959fc4bd9 100644 --- a/spec/unit/realtime/channels_spec.rb +++ b/spec/unit/realtime/channels_spec.rb @@ -1,24 +1,24 @@ # encoding: utf-8 require 'spec_helper' -describe Ably::Realtime::Channels do - let(:connection) { instance_double('Ably::Realtime::Connection', unsafe_on: true) } +describe Ably::PubSub::Realtime::Channels do + let(:connection) { instance_double('Ably::PubSub::Realtime::Connection', unsafe_on: true) } let(:client) do - instance_double('Ably::Realtime::Client', connection: connection, client_id: 'clientId', logger: double('logger').as_null_object) + instance_double('Ably::PubSub::Realtime::Client', connection: connection, client_id: 'clientId', logger: double('logger').as_null_object) end let(:channel_name) { 'unique' } let(:options) do { params: { bizarre: 'value' } } end - subject { Ably::Realtime::Channels.new(client) } + subject { Ably::PubSub::Realtime::Channels.new(client) } context 'creating channels' do context '#get' do context "when channel doesn't exist" do shared_examples 'creates a channel' do it 'creates a channel (RTS3a)' do - expect(Ably::Realtime::Channel).to receive(:new).with(client, channel_name, channel_options) + expect(Ably::PubSub::Realtime::Channel).to receive(:new).with(client, channel_name, channel_options) subject.get(channel_name, channel_options) end end @@ -42,7 +42,7 @@ shared_examples 'reuse a channel object if it exists' do it 'will reuse a channel object if it exists (RTS3a)' do channel = subject.get(channel_name, channel_options) - expect(channel).to be_a(Ably::Realtime::Channel) + expect(channel).to be_a(Ably::PubSub::Realtime::Channel) expect(subject.get(channel_name, channel_options).object_id).to eql(channel.object_id) end end @@ -79,7 +79,7 @@ end it '[] creates a channel' do - expect(Ably::Realtime::Channel).to receive(:new).with(client, channel_name, options) + expect(Ably::PubSub::Realtime::Channel).to receive(:new).with(client, channel_name, options) subject.get(channel_name, options) end end @@ -108,10 +108,10 @@ context 'is Enumerable' do let(:channel_count) { 5 } - let(:mock_channel) { instance_double('Ably::Realtime::Channel') } + let(:mock_channel) { instance_double('Ably::PubSub::Realtime::Channel') } before do - allow(Ably::Realtime::Channel).to receive(:new).and_return(mock_channel) + allow(Ably::PubSub::Realtime::Channel).to receive(:new).and_return(mock_channel) channel_count.times { |index| subject.get("channel-#{index}") } end diff --git a/spec/unit/realtime/client_spec.rb b/spec/unit/realtime/client_spec.rb index 244ff8256..9ccddb83e 100644 --- a/spec/unit/realtime/client_spec.rb +++ b/spec/unit/realtime/client_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'shared/client_initializer_behaviour' -describe Ably::Realtime::Client do +describe Ably::PubSub::Realtime::Client do subject(:realtime_client) do Ably::Internal.create_realtime_client(client_options) end @@ -13,8 +13,8 @@ let(:client_options) { { key: 'appid.keyuid:keysecret', auto_connect: false } } it 'passes on the options to the initializer' do - rest_client = instance_double('Ably::Rest::Client', auth: instance_double('Ably::Auth'), options: client_options, environment: 'production', use_tls?: true, custom_tls_port: nil) - expect(Ably::Internal).to receive(:create_rest_client).with(hash_including(client_options)).and_return(rest_client) + rest_client = instance_double('Ably::PubSub::Http::Client', auth: instance_double('Ably::Auth'), options: client_options, environment: 'production', use_tls?: true, custom_tls_port: nil) + expect(Ably::Internal).to receive(:create_http_client).with(hash_including(client_options)).and_return(rest_client) realtime_client end @@ -49,7 +49,7 @@ end specify '#push returns a Push object' do - expect(realtime_client.push).to be_a(Ably::Realtime::Push) + expect(realtime_client.push).to be_a(Ably::PubSub::Realtime::Push) end end diff --git a/spec/unit/realtime/connection_spec.rb b/spec/unit/realtime/connection_spec.rb index 9c227af0a..8bf32a927 100644 --- a/spec/unit/realtime/connection_spec.rb +++ b/spec/unit/realtime/connection_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' require 'shared/protocol_msgbus_behaviour' -describe Ably::Realtime::Connection do - let(:client) { instance_double('Ably::Realtime::Client', logger: double('logger').as_null_object, recover: nil, endpoint: double('endpoint', host: 'realtime.ably.io')) } +describe Ably::PubSub::Realtime::Connection do + let(:client) { instance_double('Ably::PubSub::Realtime::Client', logger: double('logger').as_null_object, recover: nil, endpoint: double('endpoint', host: 'realtime.ably.io')) } subject do - Ably::Realtime::Connection.new(client, {}).tap do |connection| + Ably::PubSub::Realtime::Connection.new(client, {}).tap do |connection| connection.__incoming_protocol_msgbus__.unsubscribe connection.__outgoing_protocol_msgbus__.unsubscribe end diff --git a/spec/unit/realtime/incoming_message_dispatcher_spec.rb b/spec/unit/realtime/incoming_message_dispatcher_spec.rb index 760d1e1c7..5d60d0fd3 100644 --- a/spec/unit/realtime/incoming_message_dispatcher_spec.rb +++ b/spec/unit/realtime/incoming_message_dispatcher_spec.rb @@ -1,17 +1,17 @@ require 'spec_helper' -describe Ably::Realtime::Client::IncomingMessageDispatcher, :api_private do +describe Ably::PubSub::Realtime::Client::IncomingMessageDispatcher, :api_private do let(:msgbus) do Ably::Util::PubSub.new end let(:connection) do - instance_double('Ably::Realtime::Connection', __incoming_protocol_msgbus__: msgbus, configure_new: true, id: nil, set_connection_confirmed_alive: nil) + instance_double('Ably::PubSub::Realtime::Connection', __incoming_protocol_msgbus__: msgbus, configure_new: true, id: nil, set_connection_confirmed_alive: nil) end let(:client) do - instance_double('Ably::Realtime::Client', channels: {}) + instance_double('Ably::PubSub::Realtime::Client', channels: {}) end - subject { Ably::Realtime::Client::IncomingMessageDispatcher.new(client, connection) } + subject { Ably::PubSub::Realtime::Client::IncomingMessageDispatcher.new(client, connection) } context '#initialize' do it 'should subscribe to protocol messages from the connection' do diff --git a/spec/unit/realtime/presence_spec.rb b/spec/unit/realtime/presence_spec.rb index 6588f7db8..0f6fa52dd 100644 --- a/spec/unit/realtime/presence_spec.rb +++ b/spec/unit/realtime/presence_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' require 'shared/protocol_msgbus_behaviour' -describe Ably::Realtime::Presence do - let(:channel) { double('Ably::Realtime::Channel').as_null_object } +describe Ably::PubSub::Realtime::Presence do + let(:channel) { double('Ably::PubSub::Realtime::Channel').as_null_object } subject do - Ably::Realtime::Presence.new(channel) + Ably::PubSub::Realtime::Presence.new(channel) end describe 'callbacks' do diff --git a/spec/unit/realtime/push_channel_spec.rb b/spec/unit/realtime/push_channel_spec.rb index 311e0770d..d01612006 100644 --- a/spec/unit/realtime/push_channel_spec.rb +++ b/spec/unit/realtime/push_channel_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' -describe Ably::Realtime::Channel::PushChannel do - subject { Ably::Realtime::Channel::PushChannel } +describe Ably::PubSub::Realtime::Channel::PushChannel do + subject { Ably::PubSub::Realtime::Channel::PushChannel } let(:channel_name) { 'unique' } let(:client) { double('client').as_null_object } - let(:channel) { Ably::Realtime::Channel.new(client, channel_name) } + let(:channel) { Ably::PubSub::Realtime::Channel.new(client, channel_name) } it 'is constructed with a channel' do - expect(subject.new(channel)).to be_a(Ably::Realtime::Channel::PushChannel) + expect(subject.new(channel)).to be_a(Ably::PubSub::Realtime::Channel::PushChannel) end it 'raises an exception if constructed with an invalid type' do @@ -20,12 +20,12 @@ end it 'is available in the #push attribute of the channel' do - expect(channel.push).to be_a(Ably::Realtime::Channel::PushChannel) + expect(channel.push).to be_a(Ably::PubSub::Realtime::Channel::PushChannel) expect(channel.push.channel).to eql(channel) end context 'methods not implemented as push notifications' do - subject { Ably::Realtime::Channel::PushChannel.new(channel) } + subject { Ably::PubSub::Realtime::Channel::PushChannel.new(channel) } %w(subscribe_device subscribe_client_id unsubscribe_device unsubscribe_client_id get_subscriptions).each do |method_name| specify "##{method_name} raises an unsupported exception" do diff --git a/spec/unit/realtime/realtime_spec.rb b/spec/unit/realtime/realtime_spec.rb index 2703c6ce6..9ff515f35 100644 --- a/spec/unit/realtime/realtime_spec.rb +++ b/spec/unit/realtime/realtime_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' -describe Ably::Realtime do +describe Ably::PubSub::Realtime do let(:options) { { key: 'app.key:secret', auto_connect: false } } - # This was a convenience alias for the Ably::Realtime::Client constructor. It now + # This was a convenience alias for the Ably::PubSub::Realtime::Client constructor. It now # refuses, because a client's side is declared by the package it was created from. specify 'constructor refuses and points at the factory function' do - expect { Ably::Realtime.new(options) } + expect { Ably::PubSub::Realtime.new(options) } .to raise_error(Ably::Exceptions::DirectConstructionNotSupported, /create_realtime_client/) end end diff --git a/spec/unit/realtime/recovery_key_context_spec.rb b/spec/unit/realtime/recovery_key_context_spec.rb index fd373a395..7752c9c36 100644 --- a/spec/unit/realtime/recovery_key_context_spec.rb +++ b/spec/unit/realtime/recovery_key_context_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -require 'ably/realtime/recovery_key_context' +require 'ably/pubsub/realtime/recovery_key_context' -describe Ably::Realtime::RecoveryKeyContext do +describe Ably::PubSub::Realtime::RecoveryKeyContext do context 'connection recovery key' do @@ -12,7 +12,7 @@ 'channel1' => 'serial1', 'channel2' => 'serial2' } - recovery_context = Ably::Realtime::RecoveryKeyContext.new(connection_key, msg_serial, channel_serials) + recovery_context = Ably::PubSub::Realtime::RecoveryKeyContext.new(connection_key, msg_serial, channel_serials) encoded_recovery_key = recovery_context.to_json expect(encoded_recovery_key).to eq "{\"connection_key\":\"key\",\"msg_serial\":123," << "\"channel_serials\":{\"channel1\":\"serial1\",\"channel2\":\"serial2\"}}" @@ -21,14 +21,14 @@ it 'should decode recovery key - RTN16i, RTN16f, RTN16j' do encoded_recovery_key = "{\"connection_key\":\"key\",\"msg_serial\":123," << "\"channel_serials\":{\"channel1\":\"serial1\",\"channel2\":\"serial2\"}}" - decoded_recovery_key = Ably::Realtime::RecoveryKeyContext.from_json(encoded_recovery_key) + decoded_recovery_key = Ably::PubSub::Realtime::RecoveryKeyContext.from_json(encoded_recovery_key) expect(decoded_recovery_key.connection_key).to eq("key") expect(decoded_recovery_key.msg_serial).to eq(123) end it 'should return nil for invalid recovery key - RTN16i, RTN16f, RTN16j' do encoded_recovery_key = "{\"invalid key\"}" - decoded_recovery_key = Ably::Realtime::RecoveryKeyContext.from_json(encoded_recovery_key) + decoded_recovery_key = Ably::PubSub::Realtime::RecoveryKeyContext.from_json(encoded_recovery_key) expect(decoded_recovery_key).to be_nil end diff --git a/spec/unit/realtime/safe_deferrable_spec.rb b/spec/unit/realtime/safe_deferrable_spec.rb index dfecded7e..3687753ea 100644 --- a/spec/unit/realtime/safe_deferrable_spec.rb +++ b/spec/unit/realtime/safe_deferrable_spec.rb @@ -1,7 +1,7 @@ # encoding: utf-8 require 'spec_helper' require 'shared/safe_deferrable_behaviour' -require 'ably/realtime' +require 'ably/pubsub/realtime' [Ably::Models::ProtocolMessage, Ably::Models::Message, Ably::Models::PresenceMessage].each do |model_klass| describe model_klass do diff --git a/spec/unit/realtime/websocket_transport_spec.rb b/spec/unit/realtime/websocket_transport_spec.rb index 24109fbe4..706640715 100644 --- a/spec/unit/realtime/websocket_transport_spec.rb +++ b/spec/unit/realtime/websocket_transport_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' require 'shared/protocol_msgbus_behaviour' -describe Ably::Realtime::Connection::WebsocketTransport, :api_private do - let(:client_ignored) { double('Ably::Realtime::Client').as_null_object } - let(:connection) { instance_double('Ably::Realtime::Connection', client: client_ignored, id: nil) } +describe Ably::PubSub::Realtime::Connection::WebsocketTransport, :api_private do + let(:client_ignored) { double('Ably::PubSub::Realtime::Client').as_null_object } + let(:connection) { instance_double('Ably::PubSub::Realtime::Connection', client: client_ignored, id: nil) } let(:url) { 'http://ably.io/' } let(:websocket_transport_without_eventmachine) do - Ably::Realtime::Connection::WebsocketTransport.send(:allocate).tap do |websocket_transport| + Ably::PubSub::Realtime::Connection::WebsocketTransport.send(:allocate).tap do |websocket_transport| websocket_transport.send(:initialize, connection, url) end end before do - allow(Ably::Realtime::Connection::WebsocketTransport).to receive(:new).with(connection).and_return(websocket_transport_without_eventmachine) + allow(Ably::PubSub::Realtime::Connection::WebsocketTransport).to receive(:new).with(connection).and_return(websocket_transport_without_eventmachine) end subject do - Ably::Realtime::Connection::WebsocketTransport.new(connection) + Ably::PubSub::Realtime::Connection::WebsocketTransport.new(connection) end it_behaves_like 'an incoming protocol message bus'