ld-eventsource 2.6.0 → 3.0.1

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: 1d455374db2a30af5cf5c6c94d94566eb8100e5598d013c519f098721cfba0f0
4
- data.tar.gz: 7276a718d9faa59da461f3d958f802b26c377cb51fea06eec0334c42e2721562
3
+ metadata.gz: 8b9f8e00c1bed855a376158eb3a77bb6b72192d6bb432a231df00be8c5576712
4
+ data.tar.gz: 697c406733e7590e591632493d3ff30a2f239ad3600382cf862063e2c8cf39ae
5
5
  SHA512:
6
- metadata.gz: 2b5eefef27e96b7179a5a92f71d5edeb141397a11d3c00867f4280c3e8db629714cd7cd29a1fa2365fcf5de3c5fa6240ca1890625efc95908b838d7471ff3223
7
- data.tar.gz: 932d97eb452aec82bc79e5b3a08ed43192540968e6f07c2b301437cc453e3a3baf1f662d37c24cdde9763060d8e7ec97f826378e22f1370a66250c6819bb96af
6
+ metadata.gz: 7c9a9d34b776c35cdd2f54bad3abbc53b658669996878267f375e5e36e4160f6aafad06e1275b67ce9667d81e71917f5c6b01e5daf214f9b29f8ad4f96b73c6c
7
+ data.tar.gz: 4561ccd51021a0eeb33a82528e45738d8cda462060a05a9fa17df010485d32e98817740c12611164ff7ebeb4935a12027ce11be13d98ced17864e73828e779a9
@@ -123,7 +123,7 @@ module SSE
123
123
  retry_enabled: true,
124
124
  http_client_options: nil)
125
125
  @uri = URI(uri)
126
- @stopped = Concurrent::AtomicBoolean.new(false)
126
+ @stop_event = Concurrent::Event.new
127
127
  @retry_enabled = retry_enabled
128
128
 
129
129
  @headers = headers.clone
@@ -278,7 +278,7 @@ module SSE
278
278
  # has no effect if called a second time.
279
279
  #
280
280
  def close
281
- if @stopped.make_true
281
+ if @stop_event.try?
282
282
  reset_http
283
283
  end
284
284
  end
@@ -289,7 +289,7 @@ module SSE
289
289
  # @return [Boolean] true if the client has been shut down
290
290
  #
291
291
  def closed?
292
- @stopped.value
292
+ @stop_event.set?
293
293
  end
294
294
 
295
295
  private
@@ -314,8 +314,9 @@ module SSE
314
314
  end
315
315
 
316
316
  def run_stream
317
- until @stopped.value
318
- close_connection
317
+ until @stop_event.set?
318
+ log_close_errors { close_connection }
319
+ error = nil
319
320
  begin
320
321
  resp = connect
321
322
  @lock.synchronize do
@@ -323,36 +324,42 @@ module SSE
323
324
  end
324
325
  # There's a potential race if close was called in the middle of the previous line, i.e. after we
325
326
  # connected but before @cxn was set. Checking the variable again is a bit clunky but avoids that.
326
- return if @stopped.value
327
+ if @stop_event.set?
328
+ # close did not see this connection, so close it here.
329
+ reset_http
330
+ return
331
+ end
327
332
  read_stream(resp) unless resp.nil?
328
333
  rescue => e
334
+ error = e
335
+ end
336
+ # Close the connection before the error handler runs, because the handler can block for a long time.
337
+ log_close_errors { reset_http }
338
+ unless error.nil?
329
339
  # When we deliberately close the connection, it will usually trigger an exception. The exact type
330
- # of exception depends on the specific Ruby runtime. But @stopped will always be set in this case.
331
- if @stopped.value
340
+ # of exception depends on the specific Ruby runtime. But the client will always be closed in this case.
341
+ if @stop_event.set?
332
342
  @logger.info { "Stream connection closed" }
333
343
  else
334
- log_and_dispatch_error(e, "Unexpected error from event source")
344
+ log_and_dispatch_error(error, "Unexpected error from event source")
335
345
  end
336
346
  end
337
- begin
338
- reset_http
339
- rescue StandardError => e
340
- log_and_dispatch_error(e, "Unexpected error while closing stream")
341
- end
342
347
 
343
348
  return close unless @retry_enabled
344
349
  end
345
350
  end
346
351
 
347
- # Try to establish a streaming connection. Returns the StreamingHTTPConnection object if successful.
352
+ # Try to establish a streaming connection. Returns the HTTP::Response object if successful.
348
353
  def connect
349
354
  loop do
350
- return if @stopped.value
355
+ return if @stop_event.set?
351
356
  interval = @first_attempt ? 0 : @backoff.next_interval
352
357
  @first_attempt = false
353
358
  if interval > 0
354
359
  @logger.info { "Will retry connection after #{'%.3f' % interval} seconds" }
355
- sleep(interval)
360
+ # This wait ends early when close sets the event.
361
+ @stop_event.wait(interval)
362
+ return if @stop_event.set?
356
363
  end
357
364
  cxn = nil
358
365
  begin
@@ -386,8 +393,7 @@ module SSE
386
393
  end
387
394
  end
388
395
 
389
- # Pipe the output of the StreamingHTTPConnection into the EventParser, and dispatch events as
390
- # they arrive.
396
+ # Pipe the output of the HTTP::Response into the EventParser, and dispatch events as they arrive.
391
397
  def read_stream(cxn)
392
398
  # Tell the Backoff object that the connection is now in a valid state. It uses that information so
393
399
  # it can automatically reset itself if enough time passes between failures.
@@ -395,7 +401,7 @@ module SSE
395
401
 
396
402
  chunks = Enumerator.new do |gen|
397
403
  loop do
398
- if @stopped.value
404
+ if @stop_event.set?
399
405
  break
400
406
  else
401
407
  begin
@@ -407,9 +413,9 @@ module SSE
407
413
  # For historical reasons, we rethrow this as our own type
408
414
  raise Errors::ReadTimeoutError.new(@read_timeout)
409
415
  rescue EOFError
410
- break
416
+ raise Errors::StreamClosedByServerError
411
417
  end
412
- break if data.nil? # keep for v5 compat
418
+ raise Errors::StreamClosedByServerError if data.nil? # keep for v5 compat
413
419
  gen.yield data
414
420
  end
415
421
  end
@@ -417,7 +423,7 @@ module SSE
417
423
  event_parser = Impl::EventParser.new(Impl::BufferedLineReader.lines_from(chunks), @last_id)
418
424
 
419
425
  event_parser.items.each do |item|
420
- return if @stopped.value
426
+ return if @stop_event.set?
421
427
  case item
422
428
  when StreamEvent
423
429
  dispatch_event(item)
@@ -447,6 +453,15 @@ module SSE
447
453
  end
448
454
  end
449
455
 
456
+ # A failure to close is logged but not dispatched, so each connection attempt gives the error
457
+ # handler at most one error, and the worker thread continues.
458
+ def log_close_errors
459
+ yield
460
+ rescue StandardError => e
461
+ @logger.warn { "Unexpected error while closing stream: #{e.inspect}" }
462
+ @logger.debug { "Exception trace: #{e.backtrace}" }
463
+ end
464
+
450
465
  def build_headers
451
466
  h = {
452
467
  'Accept' => 'text/event-stream',
@@ -79,5 +79,15 @@ module SSE
79
79
  super("no data received in #{interval} seconds")
80
80
  end
81
81
  end
82
+
83
+ #
84
+ # An exception class indicating that the server ended the stream without an error. This is
85
+ # passed to the error handler specified in {Client#on_error}, and the client then reconnects.
86
+ #
87
+ class StreamClosedByServerError < StandardError
88
+ def initialize
89
+ super("stream closed by server")
90
+ end
91
+ end
82
92
  end
83
93
  end
@@ -39,13 +39,13 @@ module SSE
39
39
  @attempts = 0 if good_duration >= @reconnect_reset_interval
40
40
  end
41
41
  @last_good_time = nil
42
+ # A zero base must not be multiplied: 0.0 * Infinity is NaN. Also, rand(0) is not permitted.
43
+ return 0 if @base_interval <= 0 || @max_interval <= 0
44
+
42
45
  target = [@base_interval * (2 ** @attempts), @max_interval].min.to_f
43
- @attempts += 1
44
- if target == 0
45
- 0 # in some Ruby versions it's illegal to call rand(0)
46
- else
47
- (target / 2) + @jitter_rand.rand(target / 2)
48
- end
46
+ # Stop at the ceiling, so that the power of 2 cannot grow without limit.
47
+ @attempts += 1 if target < @max_interval
48
+ (target / 2) + @jitter_rand.rand(target / 2)
49
49
  end
50
50
 
51
51
  #
@@ -1,3 +1,3 @@
1
1
  module SSE
2
- VERSION = "2.6.0" # x-release-please-version
2
+ VERSION = "3.0.1" # x-release-please-version
3
3
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ld-eventsource
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-26 00:00:00.000000000 Z
11
+ date: 2026-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: logger
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.5'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.5'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rspec
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -128,6 +114,20 @@ dependencies:
128
114
  - - "<"
129
115
  - !ruby/object:Gem::Version
130
116
  version: 7.0.0
117
+ - !ruby/object:Gem::Dependency
118
+ name: logger
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.5'
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '1.5'
131
131
  description: LaunchDarkly SSE client for Ruby
132
132
  email:
133
133
  - team@launchdarkly.com