ld-eventsource 2.2.6 → 2.4.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: 1011203906e4edf8d237499d75edc29f0c4eb4917189941fd02e9953492e5f77
4
- data.tar.gz: e2f0bfa715c896259f51c70f84f8bd3640959f7821228813c304222292a50445
3
+ metadata.gz: 5199039ee9ec2f9b9843d5bbb79dbc12ca2a625cc0d79cb99b97bb41de729a6c
4
+ data.tar.gz: 37b524d87e083d857ab4202749f6f2aac649cb0b31c04401ecfe4dfa68d082ca
5
5
  SHA512:
6
- metadata.gz: 985d8ed3a900de96dfb99bc582227903f727a097dc53826f23062f4b8585f3b76e1300381eb43703eb15f33256e26433ffb884d036c09af0e667780e7040767a
7
- data.tar.gz: 5dd455b911c275cdc98db3dbff4fd70dce1b7b6ccada654053e79639aa8351552e466b8574ce6de97cab875f5f8c7448c957ce1a33af3822f86e89ce836ba43b
6
+ metadata.gz: 6d1243db76fb427d8c5bcaae4976ce2942fe10c3ec45d4846f0b2b0d57bcb8ceb7a629da61f5f5a1f219afe29f8c1e5b8039dfad44af9b405225773499c9b4fd
7
+ data.tar.gz: 3a01a8c90f08f4740e27faf2d0789750fb15076a1e1242e45eebf09219ae500848003d09eeaa53fa16a14009383bbb424690b78419a01956d4cd7bbebfca0673
@@ -49,6 +49,9 @@ module SSE
49
49
  # The default value for `reconnect_reset_interval` in {#initialize}.
50
50
  DEFAULT_RECONNECT_RESET_INTERVAL = 60
51
51
 
52
+ # The default HTTP method for requests.
53
+ DEFAULT_HTTP_METHOD = "GET"
54
+
52
55
  #
53
56
  # Creates a new SSE client.
54
57
  #
@@ -84,6 +87,16 @@ module SSE
84
87
  # @param socket_factory [#open] (nil) an optional factory object for creating sockets,
85
88
  # if you want to use something other than the default `TCPSocket`; it must implement
86
89
  # `open(uri, timeout)` to return a connected `Socket`
90
+ # @param method [String] ("GET") the HTTP method to use for requests
91
+ # @param payload [String, Hash, Array, #call] (nil) optional request payload. If payload is a Hash or
92
+ # an Array, it will be converted to JSON and sent as the request body. A string will be sent as a non-JSON
93
+ # request body. If payload responds to #call, it will be invoked on each
94
+ # request to generate the payload dynamically.
95
+ # @param retry_enabled [Boolean] (true) whether to retry connections after failures. If false, the client
96
+ # will exit after the first connection failure instead of attempting to reconnect.
97
+ # @param http_client_options [Hash] (nil) additional options to pass to
98
+ # the HTTP client, such as `socket_factory` or `proxy`. These settings will override
99
+ # the socket factory and proxy settings.
87
100
  # @yieldparam [Client] client the new client instance, before opening the connection
88
101
  #
89
102
  def initialize(uri,
@@ -95,17 +108,25 @@ module SSE
95
108
  last_event_id: nil,
96
109
  proxy: nil,
97
110
  logger: nil,
98
- socket_factory: nil)
111
+ socket_factory: nil,
112
+ method: DEFAULT_HTTP_METHOD,
113
+ payload: nil,
114
+ retry_enabled: true,
115
+ http_client_options: nil)
99
116
  @uri = URI(uri)
100
117
  @stopped = Concurrent::AtomicBoolean.new(false)
118
+ @retry_enabled = retry_enabled
101
119
 
102
120
  @headers = headers.clone
103
121
  @connect_timeout = connect_timeout
104
122
  @read_timeout = read_timeout
123
+ @method = method.to_s.upcase
124
+ @payload = payload
105
125
  @logger = logger || default_logger
106
- http_client_options = {}
126
+
127
+ base_http_client_options = {}
107
128
  if socket_factory
108
- http_client_options["socket_class"] = socket_factory
129
+ base_http_client_options["socket_class"] = socket_factory
109
130
  end
110
131
 
111
132
  if proxy
@@ -118,13 +139,18 @@ module SSE
118
139
  end
119
140
 
120
141
  if @proxy
121
- http_client_options["proxy"] = {
142
+ base_http_client_options["proxy"] = {
122
143
  :proxy_address => @proxy.host,
123
144
  :proxy_port => @proxy.port,
124
145
  }
146
+ base_http_client_options["proxy"][:proxy_username] = @proxy.user unless @proxy.user.nil?
147
+ base_http_client_options["proxy"][:proxy_password] = @proxy.password unless @proxy.password.nil?
125
148
  end
126
149
 
127
- @http_client = HTTP::Client.new(http_client_options)
150
+ options = http_client_options.is_a?(Hash) ? base_http_client_options.merge(http_client_options) : base_http_client_options
151
+
152
+ @http_client = HTTP::Client.new(options)
153
+ .follow
128
154
  .timeout({
129
155
  read: read_timeout,
130
156
  connect: connect_timeout,
@@ -138,6 +164,7 @@ module SSE
138
164
 
139
165
  @on = { event: ->(_) {}, error: ->(_) {} }
140
166
  @last_id = last_event_id
167
+ @query_params_callback = nil
141
168
 
142
169
  yield self if block_given?
143
170
 
@@ -180,6 +207,36 @@ module SSE
180
207
  @on[:error] = action
181
208
  end
182
209
 
210
+ #
211
+ # Specifies a block or Proc to generate query parameters dynamically. This will be called before
212
+ # each connection attempt (both initial connection and reconnections), allowing you to update
213
+ # query parameters based on the current client state.
214
+ #
215
+ # The block should return a Hash with string keys and string values, which will be merged with
216
+ # any existing query parameters in the base URI. If the callback raises an exception, it will be
217
+ # logged and the connection will proceed with the base URI's query parameters (or no query
218
+ # parameters if none were present).
219
+ #
220
+ # This is useful for scenarios where query parameters need to reflect the current state of the
221
+ # client, such as sending a "basis" parameter that represents what data the client already has.
222
+ #
223
+ # @example Using dynamic query parameters
224
+ # client = SSE::Client.new(base_uri) do |c|
225
+ # c.query_params do
226
+ # {
227
+ # "basis" => (selector.state if selector.defined?),
228
+ # "filter" => filter_key
229
+ # }.compact
230
+ # end
231
+ # c.on_event { |event| handle_event(event) }
232
+ # end
233
+ #
234
+ # @yieldreturn [Hash<String, String>] a hash of query parameter names to values
235
+ #
236
+ def query_params(&action)
237
+ @query_params_callback = action
238
+ end
239
+
183
240
  #
184
241
  # Permanently shuts down the client and its connection. No further events will be dispatched. This
185
242
  # has no effect if called a second time.
@@ -246,6 +303,8 @@ module SSE
246
303
  rescue StandardError => e
247
304
  log_and_dispatch_error(e, "Unexpected error while closing stream")
248
305
  end
306
+
307
+ return close unless @retry_enabled
249
308
  end
250
309
  end
251
310
 
@@ -261,10 +320,9 @@ module SSE
261
320
  end
262
321
  cxn = nil
263
322
  begin
264
- @logger.info { "Connecting to event stream at #{@uri}" }
265
- cxn = @http_client.request("GET", @uri, {
266
- headers: build_headers,
267
- })
323
+ uri = build_uri_with_query_params
324
+ @logger.info { "Connecting to event stream at #{uri}" }
325
+ cxn = @http_client.request(@method, uri, build_opts)
268
326
  if cxn.status.code == 200
269
327
  content_type = cxn.content_type.mime_type
270
328
  if content_type && content_type.start_with?("text/event-stream")
@@ -358,5 +416,40 @@ module SSE
358
416
  h['Last-Event-Id'] = @last_id if !@last_id.nil? && @last_id != ""
359
417
  h.merge(@headers)
360
418
  end
419
+
420
+ def build_opts
421
+ return {headers: build_headers} if @payload.nil?
422
+
423
+ # Resolve payload if it's callable
424
+ resolved_payload = @payload.respond_to?(:call) ? @payload.call : @payload
425
+
426
+ if resolved_payload.is_a?(Hash) || resolved_payload.is_a?(Array)
427
+ {headers: build_headers, json: resolved_payload}
428
+ else
429
+ {headers: build_headers, body: resolved_payload.to_s}
430
+ end
431
+ end
432
+
433
+ def build_uri_with_query_params
434
+ uri = @uri.dup
435
+
436
+ if @query_params_callback
437
+ begin
438
+ dynamic_params = @query_params_callback.call
439
+ if dynamic_params.is_a?(Hash) && !dynamic_params.empty?
440
+ existing_params = uri.query ? URI.decode_www_form(uri.query).to_h : {}
441
+ merged_params = existing_params.merge(dynamic_params)
442
+ uri.query = URI.encode_www_form(merged_params)
443
+ elsif !dynamic_params.is_a?(Hash)
444
+ @logger.warn { "query_params callback returned non-Hash value: #{dynamic_params.class}, ignoring" }
445
+ end
446
+ rescue StandardError => e
447
+ @logger.warn { "query_params callback raised an exception: #{e.inspect}, proceeding with base URI" }
448
+ @logger.debug { "Exception trace: #{e.backtrace}" }
449
+ end
450
+ end
451
+
452
+ uri
453
+ end
361
454
  end
362
455
  end
@@ -42,9 +42,14 @@ module SSE
42
42
 
43
43
  pos += 1 # skip colon
44
44
  pos += 1 if pos < line.length && line[pos] == ' ' # skip optional single space, per SSE spec
45
- line = line.slice(pos..-1)
45
+ value = line.slice(pos..-1)
46
46
 
47
- item = process_field(name, line)
47
+ item = process_field(name, value)
48
+ gen.yield item unless item.nil?
49
+ else
50
+ # Handle field with no colon - treat as having empty value
51
+ # According to SSE spec, a line like "data" should be treated as "data:"
52
+ item = process_field(line, "")
48
53
  gen.yield item unless item.nil?
49
54
  end
50
55
  end
@@ -1,3 +1,3 @@
1
1
  module SSE
2
- VERSION = "2.2.6" # x-release-please-version
2
+ VERSION = "2.4.0" # x-release-please-version
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ld-eventsource
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.6
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-15 00:00:00.000000000 Z
11
+ date: 2026-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger