kameleoon-client-ruby 3.20.0 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6e939e54b9b0f43a46d27b3ce7d1d442916d036a7db9ca5159645ffc869415b
4
- data.tar.gz: 98b6e2c2b04c183ba5052379066e3eff95c9ffdc2b28963a0bd106dcf8adb0db
3
+ metadata.gz: 7f492badef67f0b4e09561d3a3d423e4497dbd7efe22d07641cac9485fe9e100
4
+ data.tar.gz: 95f7268b78ecd64bd4707c8fc8581251b8f07038a32eb044aebdb0b1cab652b6
5
5
  SHA512:
6
- metadata.gz: c4eb181ded7a566731df5e6af6bcb271fcf852c27503b5e0c44be51eb93b729decb88c378732f9a9584d5d1ef452e71a5ba651a39d20a06f7e689a3b016bc65a
7
- data.tar.gz: 997194371128c0c25cd394ab7ed22e05a0b57684915c96268874ab9e426aa3e0228f396488e77da61c2b122798eeccfee2ad1e8785900ddff24452faac953025
6
+ metadata.gz: 194b5aeee439890f068abe5b0b4c0f11fb508844de81e54e5b5582cefed5c7849d17054cb469994b57244177b61b9cfb89ccfecf771d9c135cc7126a277140e8
7
+ data.tar.gz: 114ecb570390aa32386abf7074463d903884d6faadf8c7b3de77953bc38152dc70cdf3eb7d5d8ac64de127014f318a2d2e53359c830cb9af5d8f3118d581b8d7
@@ -1,40 +1,138 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'concurrent'
4
+ require 'timeout'
5
+ require 'kameleoon/exceptions'
4
6
 
5
7
  module Kameleoon
8
+ # ClientReadiness tracks whether the SDK has successfully loaded its configuration
9
+ # and exposes that state to `KameleoonClient#wait_init` and `KameleoonClient#is_ready?`:
10
+ # - a successful fetch settles the state successfully;
11
+ # - a failed fetch settles it with an `Exception::Initialization` error.
12
+ #
13
+ # Readiness is monotonic: once the SDK is ready it stays ready, and a later failed fetch
14
+ # can never revert it. A failure reported before the first success does not prevent a
15
+ # subsequent successful retry from marking the SDK ready.
16
+ #
17
+ # It is created on the thread that builds the client but settled from the background
18
+ # thread(s) performing configuration fetches, so it relies on Concurrent::Event
19
+ # (which allows cross-thread release).
6
20
  class ClientReadiness
7
- attr_reader :is_initializing, :success
21
+ # State is a single settlement of the readiness state: `error` is written at most
22
+ # once, before the event is set, and never changes afterwards, so a waiter which
23
+ # obtained the state observes exactly the outcome it was registered for.
24
+ class State
25
+ attr_accessor :error
8
26
 
9
- def initialize
10
- @is_initializing = false
11
- @success = false
12
- @condition = Concurrent::ReadWriteLock.new
13
- reset
14
- end
27
+ def initialize
28
+ @event = Concurrent::Event.new
29
+ @error = nil
30
+ end
31
+
32
+ def settle
33
+ @event.set
34
+ end
35
+
36
+ def settled?
37
+ @event.set?
38
+ end
15
39
 
16
- def reset
17
- @success = false
18
- unless @is_initializing
19
- @is_initializing = true
20
- @condition.acquire_write_lock
40
+ # @return [Boolean] whether the state was settled before the timeout elapsed.
41
+ def wait(timeout_second)
42
+ @event.wait(timeout_second)
21
43
  end
22
44
  end
45
+ private_constant :State
46
+
47
+ def initialize(site_code, environment)
48
+ @mutex = Mutex.new
49
+ @state = State.new
50
+ @ready = false
51
+ @site_code = site_code
52
+ @environment = environment
53
+ end
23
54
 
24
- def set(success)
25
- @success = success
26
- if @is_initializing
27
- @condition.release_write_lock
28
- @is_initializing = false
55
+ # Marks the SDK ready and releases all waiters.
56
+ def mark_ready
57
+ @mutex.synchronize do
58
+ @ready = true
59
+ if @state.settled?
60
+ unless @state.error.nil?
61
+ # Recovery after a failure: the failed state is left settled for its waiters,
62
+ # and a fresh, ready state is published for the new ones.
63
+ state = State.new
64
+ state.settle
65
+ @state = state
66
+ end
67
+ else
68
+ @state.settle
69
+ end
29
70
  end
30
71
  end
31
72
 
32
- def wait
33
- if @is_initializing
34
- @condition.acquire_read_lock
35
- @condition.release_read_lock
73
+ # Reports a failed fetch with an `Exception::Initialization` error, but only while
74
+ # the SDK is not yet ready. A failure never reverts an already-ready client and is
75
+ # reported at most once, so the first reported cause is the one waiters observe.
76
+ #
77
+ # @param cause [StandardError, nil] the failure which prevented the SDK from
78
+ # loading its configuration.
79
+ def mark_not_ready(cause)
80
+ @mutex.synchronize do
81
+ unless @state.settled?
82
+ @state.error = Exception::Initialization.new(@site_code, @environment, cause)
83
+ @state.settle
84
+ end # else: already ready, or the failure was already reported
36
85
  end
37
- @success
86
+ end
87
+
88
+ # @return [Boolean] `true` if the SDK has been successfully initialized, `false`
89
+ # otherwise (including while the initialization is still pending or has failed).
90
+ # It never blocks.
91
+ #
92
+ # This is the per-request hot path, so it is lock-free: `@ready` is only ever
93
+ # flipped false -> true (readiness is monotonic), making a plain ivar read safe.
94
+ # At worst a reader briefly observes `false` while `mark_ready` is completing,
95
+ # which is indistinguishable from calling a moment earlier.
96
+ def ready?
97
+ @ready
98
+ end
99
+
100
+ # Blocks until the readiness state is settled, but no longer than `timeout_second`.
101
+ # Returns the result of the configuration fetch: nil once the SDK is ready, or the
102
+ # `Exception::Initialization` error the fetch failure was reported with (fail-fast -
103
+ # it does not wait for background retries). If no fetch result is available within
104
+ # the timeout, returns an `Exception::Initialization` error caused by the expired
105
+ # timeout. A non-positive timeout expires immediately unless a result is already
106
+ # available. An expired timeout does not settle the readiness state: once the SDK
107
+ # becomes ready, a subsequent call returns nil.
108
+ #
109
+ # @param timeout_second [Numeric] maximum number of seconds to wait.
110
+ # @return [Kameleoon::Exception::Initialization, nil] nil if the SDK is ready.
111
+ def wait_with_timeout(timeout_second)
112
+ state = current_state
113
+ return state.error if state.settled?
114
+ return timeout_failure(timeout_second) if timeout_second <= 0
115
+
116
+ return state.error if state.wait(timeout_second)
117
+
118
+ timeout_failure(timeout_second)
119
+ end
120
+
121
+ private
122
+
123
+ # The timeout failure wraps a `Timeout::Error` cause (the counterpart of the
124
+ # timeout error types the other SDKs report), so a caller can distinguish an
125
+ # expired timeout from a failed fetch.
126
+ def timeout_failure(timeout_second)
127
+ cause = Timeout::Error.new(
128
+ "initialization did not complete within #{(timeout_second * 1000).round} ms"
129
+ )
130
+ Exception::Initialization.new(@site_code, @environment, cause)
131
+ end
132
+
133
+ # Synchronized for safe publication of the state swapped in by a recovery.
134
+ def current_state
135
+ @mutex.synchronize { @state }
38
136
  end
39
137
  end
40
138
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Describes an update of the SDK data file (configuration) reported to `DataFileUpdateHandler`.
7
+ class DataFileUpdateEvent
8
+ ##
9
+ # Source of a data file update.
10
+ module Source
11
+ ##
12
+ # The data file was updated by periodic polling.
13
+ POLLING = :polling
14
+
15
+ ##
16
+ # The data file was updated by a real-time (streaming) notification.
17
+ STREAMING = :streaming
18
+ end
19
+
20
+ # @return [Symbol] The source of the data file update, one of the `Source` constants.
21
+ attr_reader :source
22
+
23
+ # @return [Integer] The date of the last modification of the data file,
24
+ # in Unix time milliseconds.
25
+ attr_reader :date_modified
26
+
27
+ def initialize(source, date_modified)
28
+ @source = source
29
+ @date_modified = date_modified
30
+ end
31
+
32
+ def to_s
33
+ "DataFileUpdateEvent{source:#{@source},date_modified:#{@date_modified}}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/events/event_handler'
4
+
5
+ module Kameleoon
6
+ module Events
7
+ ##
8
+ # Handler of `EventType::DATAFILE_UPDATE` SDK events.
9
+ #
10
+ # A handler must respond to the following method:
11
+ #
12
+ # # Called when the SDK data file (configuration) is updated.
13
+ # #
14
+ # # @param event [Kameleoon::Events::DataFileUpdateEvent] The data file update details.
15
+ # def on_update(event); end
16
+ #
17
+ # Including this module is optional and serves documentation purposes only.
18
+ module DataFileUpdateHandler
19
+ include EventHandler
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Base module for all SDK event handlers. See `EventType` for the supported event types
7
+ # and their corresponding handler modules.
8
+ #
9
+ # Including the handler modules is optional: any object which responds to the methods
10
+ # required by the selected event type is accepted by `KameleoonClient#set_event_handler`.
11
+ module EventHandler
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/events/data_file_update_event'
4
+ require 'kameleoon/events/event_type'
5
+ require 'kameleoon/events/http_request_failure'
6
+ require 'kameleoon/events/request_type'
7
+ require 'kameleoon/logging/kameleoon_logger'
8
+
9
+ module Kameleoon
10
+ module Events
11
+ ##
12
+ # EventManager stores SDK event handlers and fires SDK events.
13
+ #
14
+ # @api private
15
+ class EventManager
16
+ HANDLER_METHODS = {
17
+ EventType::HTTP_REQUEST => %i[on_request_succeeded on_request_failed],
18
+ EventType::DATAFILE_UPDATE => %i[on_update]
19
+ }.freeze
20
+
21
+ def initialize
22
+ @event_handlers = {}
23
+ end
24
+
25
+ def set_event_handler(event_type, handler)
26
+ handler_methods = HANDLER_METHODS[event_type]
27
+ if handler_methods.nil?
28
+ Logging::KameleoonLogger.error("Unknown event type '%s'", event_type)
29
+ return
30
+ end
31
+ unless handler.nil? || handler_methods.all? { |method| handler.respond_to?(method) }
32
+ Logging::KameleoonLogger.error(
33
+ "Handler for event type '%s' must respond to the following methods: %s",
34
+ event_type, handler_methods.join(', ')
35
+ )
36
+ return
37
+ end
38
+ if handler.nil?
39
+ @event_handlers.delete(event_type)
40
+ else
41
+ @event_handlers[event_type] = handler
42
+ end
43
+ end
44
+
45
+ def fire_http_request_succeeded(request_type, http_status, duration_millis)
46
+ handler = @event_handlers[EventType::HTTP_REQUEST]
47
+ return if handler.nil?
48
+
49
+ begin
50
+ handler.on_request_succeeded(request_type, http_status, duration_millis)
51
+ rescue StandardError => e
52
+ Logging::KameleoonLogger.warning('HTTP request event handler failed: %s', e)
53
+ end
54
+ end
55
+
56
+ def fire_http_request_failed(request_type, failure, duration_millis)
57
+ handler = @event_handlers[EventType::HTTP_REQUEST]
58
+ return if handler.nil?
59
+
60
+ begin
61
+ handler.on_request_failed(request_type, failure, duration_millis)
62
+ rescue StandardError => e
63
+ Logging::KameleoonLogger.warning('HTTP request event handler failed: %s', e)
64
+ end
65
+ end
66
+
67
+ def fire_data_file_update(event)
68
+ handler = @event_handlers[EventType::DATAFILE_UPDATE]
69
+ return if handler.nil?
70
+
71
+ begin
72
+ handler.on_update(event)
73
+ rescue StandardError => e
74
+ Logging::KameleoonLogger.warning('Data file update event handler failed: %s', e)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # SDK event types which can be handled with `KameleoonClient#set_event_handler`.
7
+ module EventType
8
+ ##
9
+ # HTTP request event. Requires a handler implementing the `HttpRequestHandler` methods.
10
+ HTTP_REQUEST = :http_request
11
+
12
+ ##
13
+ # Data file update event. Requires a handler implementing the `DataFileUpdateHandler` methods.
14
+ DATAFILE_UPDATE = :datafile_update
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Describes the failure of an SDK HTTP request reported to `HttpRequestHandler`.
7
+ class HttpRequestFailure
8
+ ##
9
+ # Reason of an SDK HTTP request failure.
10
+ module Reason
11
+ ##
12
+ # The request completed with an unexpected HTTP status code.
13
+ HTTP_STATUS = :http_status
14
+
15
+ ##
16
+ # The request failed with an error or exception (e.g. a network error).
17
+ EXCEPTION = :exception
18
+
19
+ ##
20
+ # The request was cancelled (for example, due to a timeout).
21
+ CANCELLED = :cancelled
22
+ end
23
+
24
+ # @return [Symbol] The reason of the failure, one of the `Reason` constants.
25
+ attr_reader :reason
26
+
27
+ # @return [Integer, nil] The HTTP status code of the response. Not `nil` only if
28
+ # `reason` is `Reason::HTTP_STATUS`.
29
+ attr_reader :http_status
30
+
31
+ # @return [Exception, nil] The exception caused the failure. Not `nil` only if
32
+ # `reason` is `Reason::EXCEPTION`.
33
+ attr_reader :cause
34
+
35
+ # @api private
36
+ def self.from_http_status(http_status)
37
+ new(Reason::HTTP_STATUS, http_status, nil)
38
+ end
39
+
40
+ # @api private
41
+ def self.from_exception(cause)
42
+ new(Reason::EXCEPTION, nil, cause)
43
+ end
44
+
45
+ # @api private
46
+ def self.of_cancellation
47
+ new(Reason::CANCELLED, nil, nil)
48
+ end
49
+
50
+ def to_s
51
+ "HttpRequestFailure{reason:#{@reason},http_status:#{@http_status || 'nil'}," \
52
+ "cause:#{@cause.nil? ? 'nil' : @cause.class}}"
53
+ end
54
+
55
+ def initialize(reason, http_status, cause)
56
+ @reason = reason
57
+ @http_status = http_status
58
+ @cause = cause
59
+ end
60
+ private_class_method :new
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/events/event_handler'
4
+
5
+ module Kameleoon
6
+ module Events
7
+ ##
8
+ # Handler of `EventType::HTTP_REQUEST` SDK events. The handler is called once per each
9
+ # actual HTTP request attempt, including retries.
10
+ #
11
+ # A handler must respond to the following methods:
12
+ #
13
+ # # Called when an SDK HTTP request completes successfully.
14
+ # #
15
+ # # @param request_type [Symbol] The type of the request, one of the `RequestType` constants.
16
+ # # @param http_status [Integer] The HTTP status code of the response.
17
+ # # @param duration_millis [Integer] The duration of the request in milliseconds.
18
+ # def on_request_succeeded(request_type, http_status, duration_millis); end
19
+ #
20
+ # # Called when an SDK HTTP request fails.
21
+ # #
22
+ # # @param request_type [Symbol] The type of the request, one of the `RequestType` constants.
23
+ # # @param failure [Kameleoon::Events::HttpRequestFailure] The failure details.
24
+ # # @param duration_millis [Integer] The duration of the request in milliseconds.
25
+ # def on_request_failed(request_type, failure, duration_millis); end
26
+ #
27
+ # Including this module is optional and serves documentation purposes only.
28
+ module HttpRequestHandler
29
+ include EventHandler
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Events
5
+ ##
6
+ # Type of an HTTP request performed by the SDK, reported to `HttpRequestHandler`.
7
+ module RequestType
8
+ ##
9
+ # Fetching of the SDK configuration (data file).
10
+ DATAFILE = :datafile
11
+
12
+ ##
13
+ # Sending of tracking data.
14
+ TRACKING = :tracking
15
+
16
+ ##
17
+ # Fetching of remote visitor data.
18
+ REMOTE_VISITOR_DATA = :remote_visitor_data
19
+
20
+ ##
21
+ # Fetching of remote data.
22
+ REMOTE_DATA = :remote_data
23
+
24
+ ##
25
+ # Fetching of the Kameleoon API access token.
26
+ ACCESS_TOKEN = :access_token
27
+ end
28
+ end
29
+ end
@@ -70,6 +70,22 @@ module Kameleoon
70
70
  class SiteCodeIsEmpty < KameleoonError
71
71
  end
72
72
 
73
+ # SDK could not be initialized: its configuration failed to load or no
74
+ # initialization result was available within the requested timeout.
75
+ # The failure which prevented the initialization is available via `cause`;
76
+ # it is also reported in the message so a caller which only logs the
77
+ # message still learns why the SDK is not initialized.
78
+ class Initialization < KameleoonError
79
+ # The failure which prevented the SDK from loading its configuration.
80
+ attr_reader :cause
81
+
82
+ def initialize(site_code, environment, cause)
83
+ @cause = cause
84
+ super("SDK is not initialized for siteCode: '#{site_code}', environment: '#{environment}'. " \
85
+ "Reason: #{cause.nil? ? 'unknown' : cause.message}")
86
+ end
87
+ end
88
+
73
89
  # Visitor Code Not Valid (empty or length > 255)
74
90
  class VisitorCodeInvalid < KameleoonError
75
91
  def initialize(visitor_code)
@@ -10,6 +10,13 @@ require 'kameleoon/data/manager/assigned_variation'
10
10
  require 'kameleoon/data/manager/forced_experiment_variation'
11
11
  require 'kameleoon/data/manager/legal_consent'
12
12
  require 'kameleoon/data/manager/visitor_manager'
13
+ require 'kameleoon/events/data_file_update_event'
14
+ require 'kameleoon/events/data_file_update_handler'
15
+ require 'kameleoon/events/event_manager'
16
+ require 'kameleoon/events/event_type'
17
+ require 'kameleoon/events/http_request_failure'
18
+ require 'kameleoon/events/http_request_handler'
19
+ require 'kameleoon/events/request_type'
13
20
  require 'kameleoon/exceptions'
14
21
  require 'kameleoon/hybrid/manager'
15
22
  require 'kameleoon/managers/data/data_manager'
@@ -22,7 +29,7 @@ require 'kameleoon/network/activity_event'
22
29
  require 'kameleoon/network/network_manager'
23
30
  require 'kameleoon/network/url_provider'
24
31
  require 'kameleoon/network/cookie/cookie_manager'
25
- require 'kameleoon/real_time/real_time_configuration_service'
32
+ require 'kameleoon/real_time/real_time_event_service'
26
33
  require 'kameleoon/storage/cache_factory'
27
34
  require 'kameleoon/targeting/models'
28
35
  require 'kameleoon/targeting/targeting_manager'
@@ -58,7 +65,8 @@ module Kameleoon
58
65
  @scheduler = Rufus::Scheduler.new
59
66
  @site_code = site_code
60
67
  @config = config
61
- @real_time_configuration_service = nil
68
+ @disposed = false
69
+ @real_time_event_service = nil
62
70
  @update_configuration_handler = nil
63
71
  @fetch_configuration_update_job = nil
64
72
  data_file = Configuration::DataFile.new(config.environment, nil)
@@ -67,11 +75,13 @@ module Kameleoon
67
75
  @data_manager, config.session_duration_second, @scheduler
68
76
  )
69
77
  @hybrid_manager = Hybrid::ManagerImpl.new(HYBRID_EXPIRATION_TIME, @data_manager)
78
+ @event_manager = Events::EventManager.new
70
79
  @network_manager = Network::NetworkManager.new(
71
80
  config.environment,
72
81
  config.default_timeout_millisecond,
73
82
  Network::AccessTokenSourceFactory.new(config.client_id, config.client_secret),
74
- Network::UrlProvider.new(site_code, config.network_domain)
83
+ Network::UrlProvider.new(site_code, config.network_domain),
84
+ @event_manager
75
85
  )
76
86
  @tracking_manager = Managers::Tracking::TrackingManager.new(
77
87
  @data_manager, @network_manager, @visitor_manager, config.tracking_interval_second, @scheduler
@@ -81,7 +91,7 @@ module Kameleoon
81
91
  @data_manager, @network_manager, @visitor_manager
82
92
  )
83
93
  @cookie_manager = Network::Cookie::CookieManager.new(@data_manager, @visitor_manager, config.top_level_domain)
84
- @readiness = ClientReadiness.new
94
+ @readiness = ClientReadiness.new(site_code, config.environment)
85
95
  @targeting_manager = Targeting::TargetingManager.new(@data_manager, @visitor_manager)
86
96
 
87
97
  if @config.verbose_mode == true && Logging::KameleoonLogger.log_level == Logging::LogLevel::WARNING
@@ -92,13 +102,56 @@ module Kameleoon
92
102
  site_code, config)
93
103
  end
94
104
 
95
- def wait_init
96
- Logging::KameleoonLogger.info('CALL: KameleoonClient.wait_init')
97
- result = @readiness.wait
105
+ ##
106
+ # Block until the SDK has loaded its configuration and is ready to use.
107
+ #
108
+ # The method returns the result of the configuration fetch: `true` once the
109
+ # SDK has been initialized, or `false` as soon as the fetch has failed
110
+ # (without waiting for background retries) or when no fetch result is
111
+ # available within the timeout. The failure which prevented the SDK from
112
+ # initializing is reported to the log.
113
+ #
114
+ # An expired timeout or a failed fetch does not affect the SDK state: the
115
+ # client keeps retrying in the background, and once the SDK becomes ready,
116
+ # a subsequent call returns true.
117
+ #
118
+ # @param [Integer, nil] timeout_millisecond Maximum time to wait, in
119
+ # milliseconds. When nil or negative, config.default_timeout_millisecond
120
+ # is applied.
121
+ # @return [Boolean] true if the SDK is ready, false if the fetch failed or
122
+ # the timeout elapsed first.
123
+ def wait_init(timeout_millisecond = nil)
124
+ Logging::KameleoonLogger.info('CALL: KameleoonClient.wait_init(timeout_millisecond: %s)', timeout_millisecond)
125
+ if timeout_millisecond.nil? || timeout_millisecond.negative?
126
+ timeout_millisecond = @config.default_timeout_millisecond
127
+ end
128
+ error = @readiness.wait_with_timeout(timeout_millisecond / 1000.0)
129
+ if error.nil?
130
+ Logging::KameleoonLogger.info('Kameleoon is initialized')
131
+ else
132
+ Logging::KameleoonLogger.error('Kameleoon failed to initialize due to error: %s', error.message)
133
+ end
134
+ result = error.nil?
98
135
  Logging::KameleoonLogger.info('RETURN: KameleoonClient.wait_init -> (result: %s)', result)
99
136
  result
100
137
  end
101
138
 
139
+ ##
140
+ # Indicates whether the SDK is ready for use, i.e. its configuration has been
141
+ # successfully loaded. Unlike `wait_init`, it returns immediately without blocking.
142
+ #
143
+ # It returns `true` if the SDK has been successfully initialized, `false` otherwise
144
+ # (including while the initialization is still pending or has failed).
145
+ #
146
+ # Readiness is monotonic: once the SDK becomes ready it stays ready.
147
+ #
148
+ # @return [Boolean] whether the SDK is ready for use.
149
+ def is_ready? # rubocop:disable Naming/PredicateName
150
+ ready = @readiness.ready?
151
+ Logging::KameleoonLogger.info('CALL/RETURN: KameleoonClient.is_ready? -> (ready: %s)', ready)
152
+ ready
153
+ end
154
+
102
155
  ##
103
156
  # Obtain a visitor code.
104
157
  #
@@ -697,6 +750,24 @@ module Kameleoon
697
750
  map_active_features
698
751
  end
699
752
 
753
+ ##
754
+ # Sets the SDK event handler for the specified event type.
755
+ #
756
+ # The handler is called when the corresponding SDK event occurs. Supported event types are
757
+ # defined in `Kameleoon::Events::EventType`. Passing `nil` clears the handler for the
758
+ # specified event type.
759
+ #
760
+ # @param event_type [Symbol] The SDK event type to handle, one of the
761
+ # `Kameleoon::Events::EventType` constants.
762
+ # @param handler [Object, nil] The handler to register, or `nil` to remove the current handler.
763
+ # The handler must respond to the methods required by the selected event type
764
+ # (see `Kameleoon::Events::HttpRequestHandler` and `Kameleoon::Events::DataFileUpdateHandler`).
765
+ def set_event_handler(event_type, handler)
766
+ Logging::KameleoonLogger.info("CALL: KameleoonClient.set_event_handler(event_type: '%s', handler)", event_type)
767
+ @event_manager.set_event_handler(event_type, handler)
768
+ Logging::KameleoonLogger.info("RETURN: KameleoonClient.set_event_handler(event_type: '%s', handler)", event_type)
769
+ end
770
+
700
771
  ##
701
772
  # The `on_update_configuration()` method allows you to handle the event when configuration
702
773
  # has updated data. It takes one input parameter: callable **handler**. The handler
@@ -704,7 +775,13 @@ module Kameleoon
704
775
  #
705
776
  # @param handler [Callable | NilClass] The handler that will be called when the configuration
706
777
  # is updated using a real-time configuration event.
778
+ #
779
+ # DEPRECATED. Please use `set_event_handler(Kameleoon::Events::EventType::DATAFILE_UPDATE, handler)` instead.
707
780
  def on_update_configuration(handler)
781
+ Logging::KameleoonLogger.info(
782
+ '[DEPRECATION] `on_update_configuration` is deprecated. ' \
783
+ 'Please use `set_event_handler(Kameleoon::Events::EventType::DATAFILE_UPDATE, handler)` instead.'
784
+ )
708
785
  Logging::KameleoonLogger.info('CALL/RETURN: KameleoonClient.on_update_configuration(handler)')
709
786
  @update_configuration_handler = handler
710
787
  end
@@ -811,40 +888,41 @@ module Kameleoon
811
888
 
812
889
  HYBRID_EXPIRATION_TIME = 5
813
890
 
814
- def fetch_configuration_initially
815
- Logging::KameleoonLogger.info('Initial configuration fetch is started.')
816
- Thread.new do
817
- ok = false
818
- begin
819
- ok = obtain_configuration
820
- Logging::KameleoonLogger.error('Initial configuration fetch failed') unless ok
821
- rescue StandardError => e
822
- Logging::KameleoonLogger.error('Initial configuration fetch failed: %s', e)
823
- end
824
- @readiness.set(ok)
825
- manage_configuration_update(@data_manager.data_file.settings.real_time_update) if ok
826
- end
827
- end
828
-
891
+ # Fetches the configuration in a background thread; used for the initial fetch,
892
+ # the polling schedule, and the streaming events alike. The fetch outcome settles
893
+ # the client readiness, and a failed fetch always falls back to the polling mode:
894
+ # a streaming event means the configuration has changed, so staying in the
895
+ # streaming mode would leave the SDK serving the outdated configuration until
896
+ # the next event arrives. The server-provided settings are not mutated, so a
897
+ # later successful fetch switches back to streaming when requested.
829
898
  def fetch_configuration_job(time_stamp = nil)
830
899
  Thread.new do
900
+ cause = nil
831
901
  ok = false
832
902
  begin
833
903
  ok = obtain_configuration(time_stamp)
834
904
  rescue StandardError => e
905
+ cause = e
835
906
  Logging::KameleoonLogger.error('Error occurred during configuration fetching: %s', e)
836
907
  end
837
- real_time_update = @data_manager.data_file.settings.real_time_update
838
- if !ok && real_time_update
839
- @data_manager.data_file.settings.real_time_update = false
840
- real_time_update = false
841
- Logging::KameleoonLogger.warning('Switching to polling mode due to failed fetch')
842
- end
843
- manage_configuration_update(real_time_update)
908
+ settle_readiness(ok, cause)
909
+ manage_configuration_update(ok && @data_manager.data_file.settings.real_time_update)
910
+ end
911
+ end
912
+
913
+ # A successful fetch marks the SDK ready, a failed one reports the failure so
914
+ # `wait_init` fails fast instead of sleeping out its timeout. A failure never
915
+ # reverts an already-ready client.
916
+ def settle_readiness(ok, cause)
917
+ if ok
918
+ @readiness.mark_ready
919
+ else
920
+ cause ||= Exception::KameleoonError.new('configuration fetch failed')
921
+ @readiness.mark_not_ready(cause)
844
922
  end
845
923
  end
846
924
 
847
- def start_configuration_update_job_if_needed
925
+ def start_polling_datafile_update
848
926
  return unless @fetch_configuration_update_job.nil?
849
927
 
850
928
  @fetch_configuration_update_job = @scheduler.schedule_every @config.refresh_interval_second do
@@ -853,7 +931,7 @@ module Kameleoon
853
931
  end
854
932
  end
855
933
 
856
- def stop_configuration_update_job_if_needed
934
+ def stop_polling_datafile_update
857
935
  return if @fetch_configuration_update_job.nil?
858
936
 
859
937
  @fetch_configuration_update_job&.unschedule
@@ -861,27 +939,34 @@ module Kameleoon
861
939
  Logging::KameleoonLogger.info('Scheduled job to fetch configuration is stopped.')
862
940
  end
863
941
 
864
- def start_real_time_configuration_service_if_needed
865
- return unless @real_time_configuration_service.nil?
942
+ def start_real_time_event_service_if_needed
943
+ return unless @real_time_event_service.nil?
866
944
 
867
945
  url = @network_manager.url_provider.make_real_time_url
868
946
  fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
869
- @real_time_configuration_service =
870
- Kameleoon::RealTime::RealTimeConfigurationService.new(url, fetch_func)
947
+ @real_time_event_service =
948
+ Kameleoon::RealTime::RealTimeEventService.new(url, fetch_func)
871
949
  end
872
950
 
873
- def stop_real_time_configuration_service_if_needed
874
- @real_time_configuration_service&.close
875
- @real_time_configuration_service = nil
951
+ def stop_real_time_event_service_if_needed
952
+ @real_time_event_service&.close
953
+ @real_time_event_service = nil
876
954
  end
877
955
 
956
+ # A disposed client must not (re)start the update services: a fetch which was
957
+ # in flight when `dispose` ran would otherwise resurrect them. `@disposed` is
958
+ # set before `dispose` stops the services, so a fetch completing after that
959
+ # point can only stop things.
878
960
  def manage_configuration_update(is_real_time_update)
879
- if is_real_time_update
880
- stop_configuration_update_job_if_needed
881
- start_real_time_configuration_service_if_needed
961
+ if @disposed
962
+ stop_polling_datafile_update
963
+ stop_real_time_event_service_if_needed
964
+ elsif is_real_time_update
965
+ stop_polling_datafile_update
966
+ start_real_time_event_service_if_needed
882
967
  else
883
- stop_real_time_configuration_service_if_needed
884
- start_configuration_update_job_if_needed
968
+ stop_real_time_event_service_if_needed
969
+ start_polling_datafile_update
885
970
  end
886
971
  end
887
972
 
@@ -908,34 +993,44 @@ module Kameleoon
908
993
  if response.configuration
909
994
  configuration = JSON.parse(response.configuration)
910
995
  data_file = Configuration::DataFile.new(@config.environment, configuration, response.last_modified)
911
- apply_new_configuration(data_file)
912
- call_update_handler_if_needed(!time_stamp.nil?)
996
+ source = time_stamp.nil? ? Events::DataFileUpdateEvent::Source::POLLING
997
+ : Events::DataFileUpdateEvent::Source::STREAMING
998
+ apply_new_configuration(data_file, source)
913
999
  Logging::KameleoonLogger.info('Feature flags are fetched: %s', response.inspect)
914
1000
  end
915
1001
  true
916
1002
  end
917
1003
 
918
- def apply_new_configuration(data_file)
919
- Logging::KameleoonLogger.debug('CALL: KameleoonClient.apply_new_configuration(data_file: %s)', data_file)
1004
+ def apply_new_configuration(data_file, source)
1005
+ Logging::KameleoonLogger.debug('CALL: KameleoonClient.apply_new_configuration(data_file: %s, source: %s)',
1006
+ data_file, source)
920
1007
  @data_manager.data_file = data_file
921
1008
  @network_manager.url_provider.apply_data_api_domain(data_file.settings.data_api_domain)
922
- Logging::KameleoonLogger.debug('RETURN: KameleoonClient.apply_new_configuration(data_file: %s)', data_file)
1009
+ @event_manager.fire_data_file_update(Events::DataFileUpdateEvent.new(source, data_file.date_modified))
1010
+ call_deprecated_update_handler if source == Events::DataFileUpdateEvent::Source::STREAMING
1011
+ Logging::KameleoonLogger.debug('RETURN: KameleoonClient.apply_new_configuration(data_file: %s, source: %s)',
1012
+ data_file, source)
923
1013
  end
924
1014
 
925
1015
  ##
926
- # Call the handler when configuration was updated with new time stamp.
927
- #
928
- # @param need_call [Bool] Indicates if we need to call handler or not.
929
- def call_update_handler_if_needed(need_call)
930
- return if !need_call || @update_configuration_handler.nil?
931
-
932
- @update_configuration_handler.call
1016
+ # Call the deprecated update configuration handler when configuration was updated
1017
+ # by a real-time (streaming) notification.
1018
+ def call_deprecated_update_handler
1019
+ handler = @update_configuration_handler
1020
+ return if handler.nil?
1021
+
1022
+ begin
1023
+ handler.call
1024
+ rescue StandardError => e
1025
+ Logging::KameleoonLogger.warning('Update configuration handler failed: %s', e)
1026
+ end
933
1027
  end
934
1028
 
935
1029
  def dispose(_object_id = nil)
936
1030
  Logging::KameleoonLogger.debug('CALL: KameleoonClient.dispose')
937
- stop_configuration_update_job_if_needed
938
- stop_real_time_configuration_service_if_needed
1031
+ @disposed = true
1032
+ stop_polling_datafile_update
1033
+ stop_real_time_event_service_if_needed
939
1034
  @visitor_manager.stop
940
1035
  @tracking_manager.stop
941
1036
  @scheduler.shutdown
@@ -24,7 +24,7 @@ module Kameleoon
24
24
  key = get_client_key(site_code, config.environment)
25
25
  client = @clients.compute_if_absent(key) do
26
26
  client = KameleoonClient.new(site_code, config)
27
- client.send(:fetch_configuration_initially)
27
+ client.send(:fetch_configuration_job)
28
28
  client
29
29
  end
30
30
  Logging::KameleoonLogger.info(
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'kameleoon/events/event_manager'
3
4
  require 'kameleoon/logging/kameleoon_logger'
4
5
  require 'kameleoon/network/content_type'
5
6
  require 'kameleoon/network/fetched_configuration'
@@ -14,8 +15,8 @@ module Kameleoon
14
15
  ##
15
16
  # NetworkManager is used to make API calls.
16
17
  class NetworkManager
17
- FETCH_CONFIGURATION_ATTEMPT_NUMBER = 3
18
- TRACKING_CALL_ATTEMPT_NUMBER = 3
18
+ FETCH_CONFIGURATION_ATTEMPT_NUMBER = 2
19
+ TRACKING_CALL_ATTEMPT_NUMBER = 2
19
20
  TRACKING_CALL_RETRY_DELAY = 5.0 # in seconds
20
21
  SDK_TYPE_HEADER = 'X-Kameleoon-SDK-Type'
21
22
  SDK_VERSION_HEADER = 'X-Kameleoon-SDK-Version'
@@ -23,13 +24,14 @@ module Kameleoon
23
24
  HEADER_IF_MODIFIED_SINCE = 'If-Modified-Since'
24
25
  HEADER_LAST_MODIFIED = 'last-modified' # in lower case because the network lib casts response headers to lower
25
26
 
26
- attr_reader :environment, :default_timeout, :access_token_source, :url_provider
27
+ attr_reader :environment, :default_timeout, :access_token_source, :url_provider, :event_manager
27
28
 
28
- def initialize(environment, default_timeout, access_token_source_factory, url_provider)
29
+ def initialize(environment, default_timeout, access_token_source_factory, url_provider, event_manager)
29
30
  @environment = environment
30
31
  @default_timeout = default_timeout
31
32
  @access_token_source = access_token_source_factory.create(self)
32
33
  @url_provider = url_provider
34
+ @event_manager = event_manager
33
35
  @sync_net_provider = SyncNetProvider.new
34
36
  end
35
37
 
@@ -39,7 +41,8 @@ module Kameleoon
39
41
  headers = { SDK_TYPE_HEADER => SDK_NAME, SDK_VERSION_HEADER => SDK_VERSION }
40
42
  headers[HEADER_IF_MODIFIED_SINCE] = if_modified_since if if_modified_since
41
43
  request = Request.new(Method::GET, url, ContentType::JSON, timeout, extra_headers: headers)
42
- response, success = make_call(request, false, FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
44
+ response, success = make_call(Events::RequestType::DATAFILE, request, false,
45
+ FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
43
46
  return nil unless success
44
47
  return FetchedConfiguration.new(nil, nil) if response.code == 304
45
48
 
@@ -51,14 +54,14 @@ module Kameleoon
51
54
  url = @url_provider.make_api_data_get_request_url(key)
52
55
  timeout = ensure_timeout(timeout)
53
56
  request = Request.new(Method::GET, url, ContentType::JSON, timeout)
54
- unwrap_response(*make_call(request, true))
57
+ unwrap_response(*make_call(Events::RequestType::REMOTE_DATA, request, true))
55
58
  end
56
59
 
57
60
  def get_remote_visitor_data(visitor_code, filter, is_unique_identifier, timeout = nil)
58
61
  url = @url_provider.make_visitor_data_get_url(visitor_code, filter, is_unique_identifier)
59
62
  timeout = ensure_timeout(timeout)
60
63
  request = Request.new(Method::GET, url, ContentType::JSON, timeout)
61
- unwrap_response(*make_call(request, true))
64
+ unwrap_response(*make_call(Events::RequestType::REMOTE_VISITOR_DATA, request, true))
62
65
  end
63
66
 
64
67
  def send_tracking_data(lines, timeout = nil)
@@ -67,7 +70,8 @@ module Kameleoon
67
70
  url = @url_provider.make_tracking_url
68
71
  timeout = ensure_timeout(timeout)
69
72
  request = Request.new(Method::POST, url, ContentType::WILDCARD, timeout, data: lines)
70
- unwrap_response(*make_call(request, true, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
73
+ unwrap_response(*make_call(Events::RequestType::TRACKING, request, true,
74
+ TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
71
75
  end
72
76
 
73
77
  def fetch_access_jwtoken(basic_auth_token, timeout = nil)
@@ -79,12 +83,12 @@ module Kameleoon
79
83
  data = UriHelper.encode_query(data_map).encode('UTF-8')
80
84
  request = Request.new(Method::POST, url, ContentType::FORM, timeout, data: data)
81
85
  request.authorize(basic_auth_token)
82
- unwrap_response(*make_call(request, false))
86
+ unwrap_response(*make_call(Events::RequestType::ACCESS_TOKEN, request, false))
83
87
  end
84
88
 
85
89
  private
86
90
 
87
- def make_call(request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
91
+ def make_call(request_type, request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
88
92
  Logging::KameleoonLogger.debug('Running request %s with access token %s, retry limit %s, retry delay %s ms',
89
93
  request, try_access_token_auth, retry_limit, retry_delay)
90
94
  attempt = 0
@@ -93,7 +97,10 @@ module Kameleoon
93
97
  while !success && (attempt <= retry_limit)
94
98
  delay(retry_delay) if attempt.positive? && retry_delay.positive?
95
99
  try_authorize(request) if try_access_token_auth
100
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
96
101
  response = @sync_net_provider.make_request(request)
102
+ duration_millis = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round
103
+ fire_http_request_event(request_type, response, duration_millis)
97
104
  if response.success?
98
105
  success = true
99
106
  elsif !response.error.nil?
@@ -122,6 +129,22 @@ module Kameleoon
122
129
  [response, success]
123
130
  end
124
131
 
132
+ def fire_http_request_event(request_type, response, duration_millis)
133
+ if response.success?
134
+ @event_manager.fire_http_request_succeeded(request_type, response.code, duration_millis)
135
+ else
136
+ failure =
137
+ if response.error.nil?
138
+ Events::HttpRequestFailure.from_http_status(response.code)
139
+ elsif response.error.is_a?(Timeout::Error)
140
+ Events::HttpRequestFailure.of_cancellation
141
+ else
142
+ Events::HttpRequestFailure.from_exception(response.error)
143
+ end
144
+ @event_manager.fire_http_request_failed(request_type, failure, duration_millis)
145
+ end
146
+ end
147
+
125
148
  def get_log_level(attempt, attempt_count)
126
149
  attempt < attempt_count ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
127
150
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Kameleoon Real Time Configuration Service
3
+ # Kameleoon Real Time Event Service
4
4
 
5
5
  require 'json'
6
6
  require 'kameleoon/logging/kameleoon_logger'
@@ -10,19 +10,24 @@ require 'kameleoon/real_time/sse_client'
10
10
 
11
11
  module Kameleoon
12
12
  module RealTime
13
- CONFIGURATION_UPDATE_EVENT = 'configuration-update-event'
13
+ DATA_FILE_UPDATE_EVENT = 'configuration-update-event'
14
14
 
15
15
  ##
16
- # RealTimeConfigurationService is used for fetching updates of configuration
16
+ # RealTimeEventService is used for fetching updates of configuration
17
17
  # (experiments and feature flags) in real time.
18
- class RealTimeConfigurationService
18
+ class RealTimeEventService
19
+ # Delay before re-establishing a failed SSE connection, so a persistent
20
+ # failure does not turn into a hot reconnect loop.
21
+ RECONNECT_DELAY_SECONDS = 3
22
+
19
23
  ##
20
24
  # Parametrized initializer.
21
25
  #
22
26
  # @param url [String]
23
27
  # @param update_handler [Callable[Kameleoon::RealTime::RealTimeEvent] | NilClass] Handler which
24
28
  # is synchronously called for gotten RealTimeEvent objects.
25
- def initialize(url, update_handler, sse_request_source = nil)
29
+ # @param reconnect_delay [Numeric] Delay in seconds before a reconnect attempt.
30
+ def initialize(url, update_handler, sse_request_source = nil, reconnect_delay: RECONNECT_DELAY_SECONDS)
26
31
  @url = url
27
32
  @update_handler = update_handler
28
33
  @need_close = false
@@ -32,6 +37,7 @@ module Kameleoon
32
37
  'Connection': 'Keep-Alive'
33
38
  }
34
39
  @sse_request_source = sse_request_source
40
+ @reconnect_delay = reconnect_delay
35
41
  @sse_thread = nil
36
42
  @sse_client = nil
37
43
  create_sse_client
@@ -42,7 +48,7 @@ module Kameleoon
42
48
  def close
43
49
  return if @need_close
44
50
 
45
- Logging::KameleoonLogger.info('Real-time configuration service is shutting down')
51
+ Logging::KameleoonLogger.info('Real-time event service is shutting down')
46
52
  @need_close = true
47
53
  return if @sse_thread.nil?
48
54
 
@@ -60,7 +66,7 @@ module Kameleoon
60
66
  def init_sse_client
61
67
  message_handler = proc do |message|
62
68
  Logging::KameleoonLogger.debug("Got SSE event: #{message.event}")
63
- if message.event == CONFIGURATION_UPDATE_EVENT
69
+ if message.event == DATA_FILE_UPDATE_EVENT
64
70
  event_dict = JSON.parse(message.data)
65
71
  @update_handler&.call(RealTimeEvent.new(event_dict))
66
72
  end
@@ -90,6 +96,9 @@ module Kameleoon
90
96
  rescue StandardError => e
91
97
  Logging::KameleoonLogger.error("Error occurred within SSE client: #{e}")
92
98
  end
99
+ # The delay is applied to every re-establishment (a failed connection and
100
+ # a dropped stream alike), so a persistent failure cannot produce a hot loop.
101
+ sleep(@reconnect_delay) unless @need_close
93
102
  end
94
103
  end
95
104
  end
@@ -5,11 +5,11 @@ require 'kameleoon/targeting/condition'
5
5
  module Kameleoon
6
6
  # @api private
7
7
  module Targeting
8
- # UnknownCondition represents not defined condition, always returns that visitor is targeted (true)
8
+ # UnknownCondition represents not defined condition, always returns that visitor is not targeted (false)
9
9
  class UnknownCondition < Condition
10
10
  def check(_data)
11
- Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as true', type)
12
- true
11
+ Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as false', type)
12
+ false
13
13
  end
14
14
  end
15
15
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.20.0'
4
+ SDK_VERSION = '3.22.0'
5
5
  SDK_NAME = 'RUBY'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kameleoon-client-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.20.0
4
+ version: 3.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kameleoon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-05 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -114,6 +114,14 @@ files:
114
114
  - lib/kameleoon/data/unique_identifier.rb
115
115
  - lib/kameleoon/data/user_agent.rb
116
116
  - lib/kameleoon/data/visitor_visits.rb
117
+ - lib/kameleoon/events/data_file_update_event.rb
118
+ - lib/kameleoon/events/data_file_update_handler.rb
119
+ - lib/kameleoon/events/event_handler.rb
120
+ - lib/kameleoon/events/event_manager.rb
121
+ - lib/kameleoon/events/event_type.rb
122
+ - lib/kameleoon/events/http_request_failure.rb
123
+ - lib/kameleoon/events/http_request_handler.rb
124
+ - lib/kameleoon/events/request_type.rb
117
125
  - lib/kameleoon/exceptions.rb
118
126
  - lib/kameleoon/hybrid/manager.rb
119
127
  - lib/kameleoon/kameleoon_client.rb
@@ -141,8 +149,8 @@ files:
141
149
  - lib/kameleoon/network/response.rb
142
150
  - lib/kameleoon/network/uri_helper.rb
143
151
  - lib/kameleoon/network/url_provider.rb
144
- - lib/kameleoon/real_time/real_time_configuration_service.rb
145
152
  - lib/kameleoon/real_time/real_time_event.rb
153
+ - lib/kameleoon/real_time/real_time_event_service.rb
146
154
  - lib/kameleoon/real_time/sse_client.rb
147
155
  - lib/kameleoon/real_time/sse_message.rb
148
156
  - lib/kameleoon/real_time/sse_request.rb