ld-eventsource 2.0.0.pre.beta.1 → 2.1.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 +5 -5
- data/README.md +1 -1
- data/lib/ld-eventsource/client.rb +43 -44
- data/lib/ld-eventsource/impl/buffered_line_reader.rb +73 -0
- data/lib/ld-eventsource/impl/event_parser.rb +6 -4
- data/lib/ld-eventsource/version.rb +1 -1
- metadata +33 -30
- data/.circleci/config.yml +0 -51
- data/.gitignore +0 -16
- data/.ldrelease/config.yml +0 -11
- data/CHANGELOG.md +0 -20
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -65
- data/ld-eventsource.gemspec +0 -29
- data/scripts/gendocs.sh +0 -12
- data/scripts/release.sh +0 -30
- data/spec/backoff_spec.rb +0 -52
- data/spec/client_spec.rb +0 -429
- data/spec/event_parser_spec.rb +0 -100
- data/spec/http_stub.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 14623ca76eeb863804646a598dec0a4f368bdd4fbf8875b7d5bb1be62c1845bd
|
4
|
+
data.tar.gz: 9c9d521d4e10f01f93f04431acfbbe2821434e99dd2738c09726827a84a96ed2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4fccea73f41283286656e7a8ad8d10c72e0175b68a34499008d38d897d9a8828000d3f9cdb582dca682e182d1d0045240d0522fd9e28e72401410aad668a603d
|
7
|
+
data.tar.gz: c0756a730b152c06e7a1497703d1acdc0216a9ad8938798914fda239011d34d4c5bf521d51f3cf4f06829e6830befb550d9a47bf6d9fa19bda9ee3e2ba08972b
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@ Parts of this code are based on https://github.com/Tonkpils/celluloid-eventsourc
|
|
10
10
|
Supported Ruby versions
|
11
11
|
-----------------------
|
12
12
|
|
13
|
-
This gem has a minimum Ruby version of 2.
|
13
|
+
This gem has a minimum Ruby version of 2.5, or 9.2 for JRuby.
|
14
14
|
|
15
15
|
Quick setup
|
16
16
|
-----------
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "ld-eventsource/impl/backoff"
|
2
|
+
require "ld-eventsource/impl/buffered_line_reader"
|
2
3
|
require "ld-eventsource/impl/event_parser"
|
3
4
|
require "ld-eventsource/events"
|
4
5
|
require "ld-eventsource/errors"
|
@@ -128,11 +129,12 @@ module SSE
|
|
128
129
|
read: read_timeout,
|
129
130
|
connect: connect_timeout
|
130
131
|
})
|
131
|
-
@
|
132
|
+
@cxn = nil
|
132
133
|
@lock = Mutex.new
|
133
134
|
|
134
135
|
@backoff = Impl::Backoff.new(reconnect_time || DEFAULT_RECONNECT_TIME, MAX_RECONNECT_TIME,
|
135
136
|
reconnect_reset_interval: reconnect_reset_interval)
|
137
|
+
@first_attempt = true
|
136
138
|
|
137
139
|
@on = { event: ->(_) {}, error: ->(_) {} }
|
138
140
|
@last_id = last_event_id
|
@@ -190,51 +192,27 @@ module SSE
|
|
190
192
|
end
|
191
193
|
end
|
192
194
|
|
195
|
+
#
|
196
|
+
# Tests whether the client has been shut down by a call to {Client#close}.
|
197
|
+
#
|
198
|
+
# @return [Boolean] true if the client has been shut down
|
199
|
+
#
|
200
|
+
def closed?
|
201
|
+
@stopped.value
|
202
|
+
end
|
203
|
+
|
193
204
|
private
|
194
205
|
|
195
206
|
def reset_http
|
196
207
|
@http_client.close if !@http_client.nil?
|
197
|
-
|
198
|
-
@buffer = ""
|
199
|
-
end
|
200
|
-
|
201
|
-
def read_lines
|
202
|
-
Enumerator.new do |gen|
|
203
|
-
loop do
|
204
|
-
line = read_line
|
205
|
-
break if line.nil?
|
206
|
-
gen.yield line
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def read_line
|
212
|
-
loop do
|
213
|
-
@lock.synchronize do
|
214
|
-
i = @buffer.index(/[\r\n]/)
|
215
|
-
if !i.nil? && !(i == @buffer.length - 1 && @buffer[i] == "\r")
|
216
|
-
i += 1 if (@buffer[i] == "\r" && @buffer[i + 1] == "\n")
|
217
|
-
return @buffer.slice!(0, i + 1).force_encoding(Encoding::UTF_8)
|
218
|
-
end
|
219
|
-
end
|
220
|
-
return nil if !read_chunk_into_buffer
|
221
|
-
end
|
208
|
+
close_connection
|
222
209
|
end
|
223
210
|
|
224
|
-
def
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
data = @cxn.readpartial
|
229
|
-
rescue HTTP::TimeoutError
|
230
|
-
# We rethrow this as our own type so the caller doesn't have to know the httprb API
|
231
|
-
raise Errors::ReadTimeoutError.new(@read_timeout)
|
211
|
+
def close_connection
|
212
|
+
@lock.synchronize do
|
213
|
+
@cxn.connection.close if !@cxn.nil?
|
214
|
+
@cxn = nil
|
232
215
|
end
|
233
|
-
return false if data == nil
|
234
|
-
@buffer << data
|
235
|
-
# We are piping the content through the parser so that it can handle things like chunked
|
236
|
-
# encoding for us. The content ends up being appended to @buffer via our callback.
|
237
|
-
true
|
238
216
|
end
|
239
217
|
|
240
218
|
def default_logger
|
@@ -246,13 +224,16 @@ module SSE
|
|
246
224
|
|
247
225
|
def run_stream
|
248
226
|
while !@stopped.value
|
249
|
-
|
227
|
+
close_connection
|
250
228
|
begin
|
251
|
-
|
229
|
+
resp = connect
|
230
|
+
@lock.synchronize do
|
231
|
+
@cxn = resp
|
232
|
+
end
|
252
233
|
# There's a potential race if close was called in the middle of the previous line, i.e. after we
|
253
234
|
# connected but before @cxn was set. Checking the variable again is a bit clunky but avoids that.
|
254
235
|
return if @stopped.value
|
255
|
-
read_stream(
|
236
|
+
read_stream(resp) if !resp.nil?
|
256
237
|
rescue => e
|
257
238
|
# When we deliberately close the connection, it will usually trigger an exception. The exact type
|
258
239
|
# of exception depends on the specific Ruby runtime. But @stopped will always be set in this case.
|
@@ -274,7 +255,8 @@ module SSE
|
|
274
255
|
def connect
|
275
256
|
loop do
|
276
257
|
return if @stopped.value
|
277
|
-
interval = @backoff.next_interval
|
258
|
+
interval = @first_attempt ? 0 : @backoff.next_interval
|
259
|
+
@first_attempt = false
|
278
260
|
if interval > 0
|
279
261
|
@logger.info { "Will retry connection after #{'%.3f' % interval} seconds" }
|
280
262
|
sleep(interval)
|
@@ -317,7 +299,24 @@ module SSE
|
|
317
299
|
# it can automatically reset itself if enough time passes between failures.
|
318
300
|
@backoff.mark_success
|
319
301
|
|
320
|
-
|
302
|
+
chunks = Enumerator.new do |gen|
|
303
|
+
loop do
|
304
|
+
if @stopped.value
|
305
|
+
break
|
306
|
+
else
|
307
|
+
begin
|
308
|
+
data = cxn.readpartial
|
309
|
+
rescue HTTP::TimeoutError
|
310
|
+
# For historical reasons, we rethrow this as our own type
|
311
|
+
raise Errors::ReadTimeoutError.new(@read_timeout)
|
312
|
+
end
|
313
|
+
break if data.nil?
|
314
|
+
gen.yield data
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
event_parser = Impl::EventParser.new(Impl::BufferedLineReader.lines_from(chunks))
|
319
|
+
|
321
320
|
event_parser.items.each do |item|
|
322
321
|
return if @stopped.value
|
323
322
|
case item
|
@@ -0,0 +1,73 @@
|
|
1
|
+
|
2
|
+
module SSE
|
3
|
+
module Impl
|
4
|
+
class BufferedLineReader
|
5
|
+
#
|
6
|
+
# Reads a series of data chunks from an enumerator, and returns an enumerator that
|
7
|
+
# parses/aggregates these into text lines. The line terminator may be CR, LF, or
|
8
|
+
# CRLF for each line; terminators are not included in the returned lines. When the
|
9
|
+
# input data runs out, the output enumerator ends and does not include any partially
|
10
|
+
# completed line.
|
11
|
+
#
|
12
|
+
# @param [Enumerator] chunks an enumerator that will yield strings from a stream
|
13
|
+
# @return [Enumerator] an enumerator that will yield one line at a time
|
14
|
+
#
|
15
|
+
def self.lines_from(chunks)
|
16
|
+
buffer = ""
|
17
|
+
position = 0
|
18
|
+
line_start = 0
|
19
|
+
last_char_was_cr = false
|
20
|
+
|
21
|
+
Enumerator.new do |gen|
|
22
|
+
chunks.each do |chunk|
|
23
|
+
buffer << chunk
|
24
|
+
|
25
|
+
loop do
|
26
|
+
# Search for a line break in any part of the buffer that we haven't yet seen.
|
27
|
+
i = buffer.index(/[\r\n]/, position)
|
28
|
+
if i.nil?
|
29
|
+
# There isn't a line break yet, so we'll keep accumulating data in the buffer, using
|
30
|
+
# position to keep track of where we left off scanning. We can also discard any previously
|
31
|
+
# parsed lines from the buffer at this point.
|
32
|
+
if line_start > 0
|
33
|
+
buffer.slice!(0, line_start)
|
34
|
+
line_start = 0
|
35
|
+
end
|
36
|
+
position = buffer.length
|
37
|
+
break
|
38
|
+
end
|
39
|
+
|
40
|
+
ch = buffer[i]
|
41
|
+
if i == 0 && ch == "\n" && last_char_was_cr
|
42
|
+
# This is just the dangling LF of a CRLF pair
|
43
|
+
last_char_was_cr = false
|
44
|
+
i += 1
|
45
|
+
position = i
|
46
|
+
line_start = i
|
47
|
+
next
|
48
|
+
end
|
49
|
+
|
50
|
+
line = buffer[line_start, i - line_start]
|
51
|
+
last_char_was_cr = false
|
52
|
+
i += 1
|
53
|
+
if ch == "\r"
|
54
|
+
if i == buffer.length
|
55
|
+
last_char_was_cr = true # We'll break the line here, but be on watch for a dangling LF
|
56
|
+
elsif buffer[i] == "\n"
|
57
|
+
i += 1
|
58
|
+
end
|
59
|
+
end
|
60
|
+
if i == buffer.length
|
61
|
+
buffer = ""
|
62
|
+
i = 0
|
63
|
+
end
|
64
|
+
position = i
|
65
|
+
line_start = i
|
66
|
+
gen.yield line
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -20,7 +20,8 @@ module SSE
|
|
20
20
|
#
|
21
21
|
# Constructs an instance of EventParser.
|
22
22
|
#
|
23
|
-
# @param [Enumerator] lines an enumerator that will yield one line of text at a time
|
23
|
+
# @param [Enumerator] lines an enumerator that will yield one line of text at a time;
|
24
|
+
# the lines should not include line terminators
|
24
25
|
#
|
25
26
|
def initialize(lines)
|
26
27
|
@lines = lines
|
@@ -31,7 +32,6 @@ module SSE
|
|
31
32
|
def items
|
32
33
|
Enumerator.new do |gen|
|
33
34
|
@lines.each do |line|
|
34
|
-
line.chomp!
|
35
35
|
if line.empty?
|
36
36
|
event = maybe_create_event
|
37
37
|
reset_buffers
|
@@ -53,6 +53,7 @@ module SSE
|
|
53
53
|
@id = nil
|
54
54
|
@type = nil
|
55
55
|
@data = ""
|
56
|
+
@have_data = false
|
56
57
|
end
|
57
58
|
|
58
59
|
def process_field(name, value)
|
@@ -60,8 +61,9 @@ module SSE
|
|
60
61
|
when "event"
|
61
62
|
@type = value.to_sym
|
62
63
|
when "data"
|
63
|
-
@data << "\n" if
|
64
|
+
@data << "\n" if @have_data
|
64
65
|
@data << value
|
66
|
+
@have_data = true
|
65
67
|
when "id"
|
66
68
|
@id = value
|
67
69
|
when "retry"
|
@@ -73,7 +75,7 @@ module SSE
|
|
73
75
|
end
|
74
76
|
|
75
77
|
def maybe_create_event
|
76
|
-
return nil if
|
78
|
+
return nil if !@have_data
|
77
79
|
StreamEvent.new(@type || :message, @data, @id)
|
78
80
|
end
|
79
81
|
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ld-eventsource
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 2.2.10
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 2.2.10
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.3.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: webrick
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.7'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: concurrent-ruby
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,16 +84,22 @@ dependencies:
|
|
70
84
|
name: http
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
|
-
- - "
|
87
|
+
- - ">="
|
74
88
|
- !ruby/object:Gem::Version
|
75
89
|
version: 4.4.1
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 6.0.0
|
76
93
|
type: :runtime
|
77
94
|
prerelease: false
|
78
95
|
version_requirements: !ruby/object:Gem::Requirement
|
79
96
|
requirements:
|
80
|
-
- - "
|
97
|
+
- - ">="
|
81
98
|
- !ruby/object:Gem::Version
|
82
99
|
version: 4.4.1
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 6.0.0
|
83
103
|
description: LaunchDarkly SSE client for Ruby
|
84
104
|
email:
|
85
105
|
- team@launchdarkly.com
|
@@ -87,28 +107,16 @@ executables: []
|
|
87
107
|
extensions: []
|
88
108
|
extra_rdoc_files: []
|
89
109
|
files:
|
90
|
-
- ".circleci/config.yml"
|
91
|
-
- ".gitignore"
|
92
|
-
- ".ldrelease/config.yml"
|
93
|
-
- CHANGELOG.md
|
94
|
-
- Gemfile
|
95
|
-
- Gemfile.lock
|
96
110
|
- LICENSE
|
97
111
|
- README.md
|
98
|
-
- ld-eventsource.gemspec
|
99
112
|
- lib/ld-eventsource.rb
|
100
113
|
- lib/ld-eventsource/client.rb
|
101
114
|
- lib/ld-eventsource/errors.rb
|
102
115
|
- lib/ld-eventsource/events.rb
|
103
116
|
- lib/ld-eventsource/impl/backoff.rb
|
117
|
+
- lib/ld-eventsource/impl/buffered_line_reader.rb
|
104
118
|
- lib/ld-eventsource/impl/event_parser.rb
|
105
119
|
- lib/ld-eventsource/version.rb
|
106
|
-
- scripts/gendocs.sh
|
107
|
-
- scripts/release.sh
|
108
|
-
- spec/backoff_spec.rb
|
109
|
-
- spec/client_spec.rb
|
110
|
-
- spec/event_parser_spec.rb
|
111
|
-
- spec/http_stub.rb
|
112
120
|
homepage: https://github.com/launchdarkly/ruby-eventsource
|
113
121
|
licenses:
|
114
122
|
- Apache-2.0
|
@@ -124,17 +132,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
124
132
|
version: '0'
|
125
133
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
134
|
requirements:
|
127
|
-
- - "
|
135
|
+
- - ">="
|
128
136
|
- !ruby/object:Gem::Version
|
129
|
-
version:
|
137
|
+
version: '0'
|
130
138
|
requirements: []
|
131
|
-
|
132
|
-
rubygems_version: 2.5.2.3
|
139
|
+
rubygems_version: 3.2.29
|
133
140
|
signing_key:
|
134
141
|
specification_version: 4
|
135
142
|
summary: LaunchDarkly SSE client
|
136
|
-
test_files:
|
137
|
-
- spec/backoff_spec.rb
|
138
|
-
- spec/client_spec.rb
|
139
|
-
- spec/event_parser_spec.rb
|
140
|
-
- spec/http_stub.rb
|
143
|
+
test_files: []
|
data/.circleci/config.yml
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
version: 2
|
2
|
-
|
3
|
-
workflows:
|
4
|
-
version: 2
|
5
|
-
test:
|
6
|
-
jobs:
|
7
|
-
- test-2.4
|
8
|
-
- test-2.5
|
9
|
-
- test-2.6
|
10
|
-
- test-2.7
|
11
|
-
- test-jruby-9.2
|
12
|
-
|
13
|
-
ruby-docker-template: &ruby-docker-template
|
14
|
-
steps:
|
15
|
-
- checkout
|
16
|
-
- run: |
|
17
|
-
if [[ $CIRCLE_JOB == test-jruby* ]]; then
|
18
|
-
gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
|
19
|
-
fi
|
20
|
-
- run: sudo apt-get update -y && sudo apt-get install -y build-essential
|
21
|
-
- run: ruby -v
|
22
|
-
- run: gem install bundler -v "~> 1.17"
|
23
|
-
- run: bundle install
|
24
|
-
- run: mkdir ./rspec
|
25
|
-
- run: bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
|
26
|
-
- store_test_results:
|
27
|
-
path: ./rspec
|
28
|
-
- store_artifacts:
|
29
|
-
path: ./rspec
|
30
|
-
|
31
|
-
jobs:
|
32
|
-
test-2.4:
|
33
|
-
<<: *ruby-docker-template
|
34
|
-
docker:
|
35
|
-
- image: circleci/ruby:2.4
|
36
|
-
test-2.5:
|
37
|
-
<<: *ruby-docker-template
|
38
|
-
docker:
|
39
|
-
- image: circleci/ruby:2.5
|
40
|
-
test-2.6:
|
41
|
-
<<: *ruby-docker-template
|
42
|
-
docker:
|
43
|
-
- image: circleci/ruby:2.6
|
44
|
-
test-2.7:
|
45
|
-
<<: *ruby-docker-template
|
46
|
-
docker:
|
47
|
-
- image: circleci/ruby:2.7
|
48
|
-
test-jruby-9.2:
|
49
|
-
<<: *ruby-docker-template
|
50
|
-
docker:
|
51
|
-
- image: circleci/jruby:9.2-jdk
|
data/.gitignore
DELETED
data/.ldrelease/config.yml
DELETED
data/CHANGELOG.md
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# Change log
|
2
|
-
|
3
|
-
All notable changes to the LaunchDarkly SSE Client for Ruby will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
4
|
-
|
5
|
-
## [1.0.3] - 2020-03-17
|
6
|
-
### Fixed:
|
7
|
-
- The backoff delay logic for reconnecting after a stream failure was broken so that if a failure occurred after a stream had been active for at least `reconnect_reset_interval` (default 60 seconds), retries would use _no_ delay, potentially causing a flood of requests and a spike in CPU usage.
|
8
|
-
|
9
|
-
## [1.0.2] - 2020-03-10
|
10
|
-
### Removed:
|
11
|
-
- Removed an unused dependency on `rake`. There are no other changes in this release.
|
12
|
-
|
13
|
-
|
14
|
-
## [1.0.1] - 2019-07-10
|
15
|
-
### Fixed:
|
16
|
-
- Calling `close` on the client could cause a misleading warning message in the log, such as `Unexpected error from event source: #<IOError: stream closed in another thread>`.
|
17
|
-
|
18
|
-
## [1.0.0] - 2019-01-03
|
19
|
-
|
20
|
-
Initial release.
|
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
@@ -1,65 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
ld-eventsource (2.0.0.pre.beta.1)
|
5
|
-
concurrent-ruby (~> 1.0)
|
6
|
-
http (~> 4.4.1)
|
7
|
-
|
8
|
-
GEM
|
9
|
-
remote: https://rubygems.org/
|
10
|
-
specs:
|
11
|
-
addressable (2.7.0)
|
12
|
-
public_suffix (>= 2.0.2, < 5.0)
|
13
|
-
concurrent-ruby (1.1.8)
|
14
|
-
diff-lcs (1.3)
|
15
|
-
domain_name (0.5.20190701)
|
16
|
-
unf (>= 0.0.5, < 1.0.0)
|
17
|
-
ffi (1.14.2)
|
18
|
-
ffi (1.14.2-java)
|
19
|
-
ffi-compiler (1.0.1)
|
20
|
-
ffi (>= 1.0.0)
|
21
|
-
rake
|
22
|
-
http (4.4.1)
|
23
|
-
addressable (~> 2.3)
|
24
|
-
http-cookie (~> 1.0)
|
25
|
-
http-form_data (~> 2.2)
|
26
|
-
http-parser (~> 1.2.0)
|
27
|
-
http-cookie (1.0.3)
|
28
|
-
domain_name (~> 0.5)
|
29
|
-
http-form_data (2.3.0)
|
30
|
-
http-parser (1.2.3)
|
31
|
-
ffi-compiler (>= 1.0, < 2.0)
|
32
|
-
public_suffix (4.0.6)
|
33
|
-
rake (13.0.3)
|
34
|
-
rspec (3.7.0)
|
35
|
-
rspec-core (~> 3.7.0)
|
36
|
-
rspec-expectations (~> 3.7.0)
|
37
|
-
rspec-mocks (~> 3.7.0)
|
38
|
-
rspec-core (3.7.1)
|
39
|
-
rspec-support (~> 3.7.0)
|
40
|
-
rspec-expectations (3.7.0)
|
41
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
42
|
-
rspec-support (~> 3.7.0)
|
43
|
-
rspec-mocks (3.7.0)
|
44
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
45
|
-
rspec-support (~> 3.7.0)
|
46
|
-
rspec-support (3.7.0)
|
47
|
-
rspec_junit_formatter (0.3.0)
|
48
|
-
rspec-core (>= 2, < 4, != 2.12.0)
|
49
|
-
unf (0.1.4)
|
50
|
-
unf_ext
|
51
|
-
unf (0.1.4-java)
|
52
|
-
unf_ext (0.0.7.7)
|
53
|
-
|
54
|
-
PLATFORMS
|
55
|
-
java
|
56
|
-
ruby
|
57
|
-
|
58
|
-
DEPENDENCIES
|
59
|
-
bundler (~> 1.7)
|
60
|
-
ld-eventsource!
|
61
|
-
rspec (~> 3.2)
|
62
|
-
rspec_junit_formatter (~> 0.3.0)
|
63
|
-
|
64
|
-
BUNDLED WITH
|
65
|
-
1.17.3
|
data/ld-eventsource.gemspec
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
lib = File.expand_path("../lib", __FILE__)
|
4
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require "ld-eventsource/version"
|
6
|
-
|
7
|
-
# rubocop:disable Metrics/BlockLength
|
8
|
-
Gem::Specification.new do |spec|
|
9
|
-
spec.name = "ld-eventsource"
|
10
|
-
spec.version = SSE::VERSION
|
11
|
-
spec.authors = ["LaunchDarkly"]
|
12
|
-
spec.email = ["team@launchdarkly.com"]
|
13
|
-
spec.summary = "LaunchDarkly SSE client"
|
14
|
-
spec.description = "LaunchDarkly SSE client for Ruby"
|
15
|
-
spec.homepage = "https://github.com/launchdarkly/ruby-eventsource"
|
16
|
-
spec.license = "Apache-2.0"
|
17
|
-
|
18
|
-
spec.files = `git ls-files -z`.split("\x0")
|
19
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
-
spec.require_paths = ["lib"]
|
22
|
-
|
23
|
-
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
-
spec.add_development_dependency "rspec", "~> 3.2"
|
25
|
-
spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
|
26
|
-
|
27
|
-
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
28
|
-
spec.add_runtime_dependency "http", "~> 4.4.1"
|
29
|
-
end
|
data/scripts/gendocs.sh
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
# Use this script to generate documentation locally in ./doc so it can be proofed before release.
|
4
|
-
# After release, documentation will be visible at https://www.rubydoc.info/gems/ld-eventsource
|
5
|
-
|
6
|
-
gem install --conservative yard
|
7
|
-
gem install --conservative redcarpet # provides Markdown formatting
|
8
|
-
|
9
|
-
# yard doesn't seem to do recursive directories, even though Ruby's Dir.glob supposedly recurses for "**"
|
10
|
-
PATHS="lib/*.rb lib/**/*.rb lib/**/**/*.rb"
|
11
|
-
|
12
|
-
yard doc --no-private --markup markdown --markup-provider redcarpet --embed-mixins $PATHS - README.md
|
data/scripts/release.sh
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
#!/usr/bin/env bash
|
2
|
-
|
3
|
-
# This script updates the version for the library and releases it to RubyGems
|
4
|
-
# It will only work if you have the proper credentials set up in ~/.gem/credentials
|
5
|
-
|
6
|
-
# It takes exactly one argument: the new version.
|
7
|
-
# It should be run from the root of this git repo like this:
|
8
|
-
# ./scripts/release.sh 4.0.9
|
9
|
-
|
10
|
-
# When done you should commit and push the changes made.
|
11
|
-
|
12
|
-
set -uxe
|
13
|
-
|
14
|
-
VERSION=$1
|
15
|
-
GEM_NAME=ld-eventsource
|
16
|
-
|
17
|
-
echo "Starting $GEM_NAME release."
|
18
|
-
|
19
|
-
# Update version in version.rb
|
20
|
-
VERSION_RB_TEMP=./version.rb.tmp
|
21
|
-
sed "s/VERSION =.*/VERSION = \"${VERSION}\"/g" lib/$GEM_NAME/version.rb > ${VERSION_RB_TEMP}
|
22
|
-
mv ${VERSION_RB_TEMP} lib/$GEM_NAME/version.rb
|
23
|
-
|
24
|
-
# Build Ruby gem
|
25
|
-
gem build $GEM_NAME.gemspec
|
26
|
-
|
27
|
-
# Publish Ruby gem
|
28
|
-
gem push $GEM_NAME-${VERSION}.gem
|
29
|
-
|
30
|
-
echo "Done with $GEM_NAME release"
|
data/spec/backoff_spec.rb
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
require "ld-eventsource"
|
2
|
-
|
3
|
-
require "http_stub"
|
4
|
-
|
5
|
-
module SSE
|
6
|
-
module Impl
|
7
|
-
describe Backoff do
|
8
|
-
it "increases exponentially with jitter" do
|
9
|
-
initial = 1.5
|
10
|
-
max = 60
|
11
|
-
b = Backoff.new(initial, max)
|
12
|
-
previous = 0
|
13
|
-
|
14
|
-
for i in 1..6 do
|
15
|
-
interval = b.next_interval
|
16
|
-
expect(interval).to be > previous
|
17
|
-
target = initial * (2 ** (i - 1))
|
18
|
-
expect(interval).to be <= target
|
19
|
-
expect(interval).to be >= target / 2
|
20
|
-
previous = i
|
21
|
-
end
|
22
|
-
|
23
|
-
interval = b.next_interval
|
24
|
-
expect(interval).to be >= previous
|
25
|
-
expect(interval).to be <= max
|
26
|
-
end
|
27
|
-
|
28
|
-
it "resets to initial delay if reset threshold has elapsed" do
|
29
|
-
initial = 1.5
|
30
|
-
max = 60
|
31
|
-
threshold = 2
|
32
|
-
b = Backoff.new(initial, max, reconnect_reset_interval: threshold)
|
33
|
-
|
34
|
-
for i in 1..6 do
|
35
|
-
# just cause the backoff to increase quickly, don't actually do these delays
|
36
|
-
b.next_interval
|
37
|
-
end
|
38
|
-
|
39
|
-
b.mark_success
|
40
|
-
sleep(threshold + 0.001)
|
41
|
-
|
42
|
-
interval = b.next_interval
|
43
|
-
expect(interval).to be <= initial
|
44
|
-
expect(interval).to be >= initial / 2
|
45
|
-
|
46
|
-
interval = b.next_interval # make sure it continues increasing after that
|
47
|
-
expect(interval).to be <= (initial * 2)
|
48
|
-
expect(interval).to be >= initial
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
data/spec/client_spec.rb
DELETED
@@ -1,429 +0,0 @@
|
|
1
|
-
require "ld-eventsource"
|
2
|
-
|
3
|
-
#
|
4
|
-
# End-to-end tests of the SSE client against a real server
|
5
|
-
#
|
6
|
-
describe SSE::Client do
|
7
|
-
subject { SSE::Client }
|
8
|
-
|
9
|
-
let(:simple_event_1) { SSE::StreamEvent.new(:go, "foo", "a")}
|
10
|
-
let(:simple_event_2) { SSE::StreamEvent.new(:stop, "bar", "b")}
|
11
|
-
let(:simple_event_1_text) { <<-EOT
|
12
|
-
event: go
|
13
|
-
data: foo
|
14
|
-
id: a
|
15
|
-
|
16
|
-
EOT
|
17
|
-
}
|
18
|
-
let(:simple_event_2_text) { <<-EOT
|
19
|
-
event: stop
|
20
|
-
data: bar
|
21
|
-
id: b
|
22
|
-
|
23
|
-
EOT
|
24
|
-
}
|
25
|
-
let(:reconnect_asap) { 0.01 }
|
26
|
-
|
27
|
-
def with_client(client)
|
28
|
-
begin
|
29
|
-
yield client
|
30
|
-
ensure
|
31
|
-
client.close
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def send_stream_content(res, content, keep_open:)
|
36
|
-
res.content_type = "text/event-stream"
|
37
|
-
res.status = 200
|
38
|
-
res.chunked = true
|
39
|
-
rd, wr = IO.pipe
|
40
|
-
wr.write(content)
|
41
|
-
res.body = rd
|
42
|
-
if !keep_open
|
43
|
-
wr.close
|
44
|
-
end
|
45
|
-
wr
|
46
|
-
end
|
47
|
-
|
48
|
-
it "sends expected headers" do
|
49
|
-
with_server do |server|
|
50
|
-
requests = Queue.new
|
51
|
-
server.setup_response("/") do |req,res|
|
52
|
-
requests << req
|
53
|
-
send_stream_content(res, "", keep_open: true)
|
54
|
-
end
|
55
|
-
|
56
|
-
headers = { "Authorization" => "secret" }
|
57
|
-
|
58
|
-
with_client(subject.new(server.base_uri, headers: headers)) do |client|
|
59
|
-
received_req = requests.pop
|
60
|
-
expect(received_req.header).to eq({
|
61
|
-
"accept" => ["text/event-stream"],
|
62
|
-
"cache-control" => ["no-cache"],
|
63
|
-
"host" => ["127.0.0.1:" + server.port.to_s],
|
64
|
-
"authorization" => ["secret"],
|
65
|
-
"user-agent" => ["ruby-eventsource"],
|
66
|
-
"content-length" => ["0"],
|
67
|
-
"connection" => ["close"]
|
68
|
-
})
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
it "sends initial Last-Event-Id if specified" do
|
74
|
-
id = "xyz"
|
75
|
-
with_server do |server|
|
76
|
-
requests = Queue.new
|
77
|
-
server.setup_response("/") do |req,res|
|
78
|
-
requests << req
|
79
|
-
send_stream_content(res, "", keep_open: true)
|
80
|
-
end
|
81
|
-
|
82
|
-
headers = { "Authorization" => "secret" }
|
83
|
-
|
84
|
-
with_client(subject.new(server.base_uri, headers: headers, last_event_id: id)) do |client|
|
85
|
-
received_req = requests.pop
|
86
|
-
expect(received_req.header).to eq({
|
87
|
-
"accept" => ["text/event-stream"],
|
88
|
-
"cache-control" => ["no-cache"],
|
89
|
-
"host" => ["127.0.0.1:" + server.port.to_s],
|
90
|
-
"authorization" => ["secret"],
|
91
|
-
"last-event-id" => [id],
|
92
|
-
"user-agent" => ["ruby-eventsource"],
|
93
|
-
"content-length" => ["0"],
|
94
|
-
"connection" => ["close"]
|
95
|
-
})
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
it "receives messages" do
|
101
|
-
events_body = simple_event_1_text + simple_event_2_text
|
102
|
-
with_server do |server|
|
103
|
-
server.setup_response("/") do |req,res|
|
104
|
-
send_stream_content(res, events_body, keep_open: true)
|
105
|
-
end
|
106
|
-
|
107
|
-
event_sink = Queue.new
|
108
|
-
client = subject.new(server.base_uri) do |c|
|
109
|
-
c.on_event { |event| event_sink << event }
|
110
|
-
end
|
111
|
-
|
112
|
-
with_client(client) do |client|
|
113
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
114
|
-
expect(event_sink.pop).to eq(simple_event_2)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
it "does not trigger an error when stream is closed" do
|
120
|
-
events_body = simple_event_1_text + simple_event_2_text
|
121
|
-
with_server do |server|
|
122
|
-
server.setup_response("/") do |req,res|
|
123
|
-
send_stream_content(res, events_body, keep_open: true)
|
124
|
-
end
|
125
|
-
|
126
|
-
event_sink = Queue.new
|
127
|
-
error_sink = Queue.new
|
128
|
-
client = subject.new(server.base_uri) do |c|
|
129
|
-
c.on_event { |event| event_sink << event }
|
130
|
-
c.on_error { |error| error_sink << error }
|
131
|
-
end
|
132
|
-
|
133
|
-
with_client(client) do |client|
|
134
|
-
event_sink.pop # wait till we have definitely started reading the stream
|
135
|
-
client.close
|
136
|
-
sleep 0.25 # there's no way to really know when the stream thread has finished
|
137
|
-
expect(error_sink.empty?).to be true
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
it "reconnects after error response" do
|
143
|
-
events_body = simple_event_1_text
|
144
|
-
with_server do |server|
|
145
|
-
attempt = 0
|
146
|
-
server.setup_response("/") do |req,res|
|
147
|
-
attempt += 1
|
148
|
-
if attempt == 1
|
149
|
-
res.status = 500
|
150
|
-
res.body = "sorry"
|
151
|
-
res.keep_alive = false
|
152
|
-
else
|
153
|
-
send_stream_content(res, events_body, keep_open: true)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
event_sink = Queue.new
|
158
|
-
error_sink = Queue.new
|
159
|
-
client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
|
160
|
-
c.on_event { |event| event_sink << event }
|
161
|
-
c.on_error { |error| error_sink << error }
|
162
|
-
end
|
163
|
-
|
164
|
-
with_client(client) do |client|
|
165
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
166
|
-
expect(error_sink.pop).to eq(SSE::Errors::HTTPStatusError.new(500, "sorry"))
|
167
|
-
expect(attempt).to eq 2
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
it "reconnects after invalid content type" do
|
173
|
-
events_body = simple_event_1_text
|
174
|
-
with_server do |server|
|
175
|
-
attempt = 0
|
176
|
-
server.setup_response("/") do |req,res|
|
177
|
-
attempt += 1
|
178
|
-
if attempt == 1
|
179
|
-
res.status = 200
|
180
|
-
res.content_type = "text/plain"
|
181
|
-
res.body = "sorry"
|
182
|
-
res.keep_alive = false
|
183
|
-
else
|
184
|
-
send_stream_content(res, events_body, keep_open: true)
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
event_sink = Queue.new
|
189
|
-
error_sink = Queue.new
|
190
|
-
client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
|
191
|
-
c.on_event { |event| event_sink << event }
|
192
|
-
c.on_error { |error| error_sink << error }
|
193
|
-
end
|
194
|
-
|
195
|
-
with_client(client) do |client|
|
196
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
197
|
-
expect(error_sink.pop).to eq(SSE::Errors::HTTPContentTypeError.new("text/plain"))
|
198
|
-
expect(attempt).to eq 2
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
it "reconnects after read timeout" do
|
204
|
-
events_body = simple_event_1_text
|
205
|
-
with_server do |server|
|
206
|
-
attempt = 0
|
207
|
-
server.setup_response("/") do |req,res|
|
208
|
-
attempt += 1
|
209
|
-
if attempt == 1
|
210
|
-
sleep(1)
|
211
|
-
end
|
212
|
-
send_stream_content(res, events_body, keep_open: true)
|
213
|
-
end
|
214
|
-
|
215
|
-
event_sink = Queue.new
|
216
|
-
client = subject.new(server.base_uri, reconnect_time: reconnect_asap, read_timeout: 0.25) do |c|
|
217
|
-
c.on_event { |event| event_sink << event }
|
218
|
-
end
|
219
|
-
|
220
|
-
with_client(client) do |client|
|
221
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
222
|
-
expect(attempt).to eq 2
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
it "reconnects if stream returns EOF" do
|
228
|
-
with_server do |server|
|
229
|
-
attempt = 0
|
230
|
-
server.setup_response("/") do |req,res|
|
231
|
-
attempt += 1
|
232
|
-
send_stream_content(res, attempt == 1 ? simple_event_1_text : simple_event_2_text,
|
233
|
-
keep_open: attempt == 2)
|
234
|
-
end
|
235
|
-
|
236
|
-
event_sink = Queue.new
|
237
|
-
client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
|
238
|
-
c.on_event { |event| event_sink << event }
|
239
|
-
end
|
240
|
-
|
241
|
-
with_client(client) do |client|
|
242
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
243
|
-
expect(event_sink.pop).to eq(simple_event_2)
|
244
|
-
expect(attempt).to eq 2
|
245
|
-
end
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
|
-
it "sends ID of last received event, if any, when reconnecting" do
|
250
|
-
with_server do |server|
|
251
|
-
requests = Queue.new
|
252
|
-
attempt = 0
|
253
|
-
server.setup_response("/") do |req,res|
|
254
|
-
requests << req
|
255
|
-
attempt += 1
|
256
|
-
send_stream_content(res, attempt == 1 ? simple_event_1_text : simple_event_2_text,
|
257
|
-
keep_open: attempt == 2)
|
258
|
-
end
|
259
|
-
|
260
|
-
event_sink = Queue.new
|
261
|
-
client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
|
262
|
-
c.on_event { |event| event_sink << event }
|
263
|
-
end
|
264
|
-
|
265
|
-
with_client(client) do |client|
|
266
|
-
req1 = requests.pop
|
267
|
-
req2 = requests.pop
|
268
|
-
expect(req2.header["last-event-id"]).to eq([ simple_event_1.id ])
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
it "increases backoff delay if a failure happens within the reset threshold" do
|
274
|
-
request_times = []
|
275
|
-
max_requests = 5
|
276
|
-
initial_interval = 0.25
|
277
|
-
|
278
|
-
with_server do |server|
|
279
|
-
attempt = 0
|
280
|
-
server.setup_response("/") do |req,res|
|
281
|
-
request_times << Time.now
|
282
|
-
attempt += 1
|
283
|
-
send_stream_content(res, simple_event_1_text, keep_open: attempt == max_requests)
|
284
|
-
end
|
285
|
-
|
286
|
-
event_sink = Queue.new
|
287
|
-
client = subject.new(server.base_uri, reconnect_time: initial_interval) do |c|
|
288
|
-
c.on_event { |event| event_sink << event }
|
289
|
-
end
|
290
|
-
|
291
|
-
with_client(client) do |client|
|
292
|
-
last_interval = nil
|
293
|
-
max_requests.times do |i|
|
294
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
295
|
-
if i > 0
|
296
|
-
interval = request_times[i] - request_times[i - 1]
|
297
|
-
minimum_expected_interval = initial_interval * (2 ** (i - 1)) / 2
|
298
|
-
expect(interval).to be >= minimum_expected_interval
|
299
|
-
last_interval = interval
|
300
|
-
end
|
301
|
-
end
|
302
|
-
end
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
it "resets backoff delay if a failure happens after the reset threshold" do
|
307
|
-
request_times = []
|
308
|
-
request_end_times = []
|
309
|
-
max_requests = 5
|
310
|
-
threshold = 0.3
|
311
|
-
initial_interval = 0.25
|
312
|
-
|
313
|
-
with_server do |server|
|
314
|
-
attempt = 0
|
315
|
-
server.setup_response("/") do |req,res|
|
316
|
-
request_times << Time.now
|
317
|
-
attempt += 1
|
318
|
-
stream = send_stream_content(res, simple_event_1_text, keep_open: true)
|
319
|
-
Thread.new do
|
320
|
-
sleep(threshold + 0.01)
|
321
|
-
stream.close
|
322
|
-
request_end_times << Time.now
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
event_sink = Queue.new
|
327
|
-
client = subject.new(server.base_uri, reconnect_time: initial_interval, reconnect_reset_interval: threshold) do |c|
|
328
|
-
c.on_event { |event| event_sink << event }
|
329
|
-
end
|
330
|
-
|
331
|
-
with_client(client) do |client|
|
332
|
-
last_interval = nil
|
333
|
-
max_requests.times do |i|
|
334
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
335
|
-
if i > 0
|
336
|
-
interval = request_times[i] - request_end_times[i - 1]
|
337
|
-
expect(interval).to be <= (initial_interval + 0.1)
|
338
|
-
end
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
342
|
-
end
|
343
|
-
|
344
|
-
it "can change initial reconnect delay based on directive from server" do
|
345
|
-
request_times = []
|
346
|
-
configured_interval = 1
|
347
|
-
retry_ms = 100
|
348
|
-
|
349
|
-
with_server do |server|
|
350
|
-
attempt = 0
|
351
|
-
server.setup_response("/") do |req,res|
|
352
|
-
request_times << Time.now
|
353
|
-
attempt += 1
|
354
|
-
if attempt == 1
|
355
|
-
send_stream_content(res, "retry: #{retry_ms}\n", keep_open: false)
|
356
|
-
else
|
357
|
-
send_stream_content(res, simple_event_1_text, keep_open: true)
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
|
-
event_sink = Queue.new
|
362
|
-
client = subject.new(server.base_uri, reconnect_time: configured_interval) do |c|
|
363
|
-
c.on_event { |event| event_sink << event }
|
364
|
-
end
|
365
|
-
|
366
|
-
with_client(client) do |client|
|
367
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
368
|
-
interval = request_times[1] - request_times[0]
|
369
|
-
expect(interval).to be < 0.5
|
370
|
-
end
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
|
-
it "connects to HTTP server through proxy" do
|
375
|
-
events_body = simple_event_1_text
|
376
|
-
with_server do |server|
|
377
|
-
server.setup_response("/") do |req,res|
|
378
|
-
send_stream_content(res, events_body, keep_open: false)
|
379
|
-
end
|
380
|
-
with_server(StubProxyServer.new) do |proxy|
|
381
|
-
event_sink = Queue.new
|
382
|
-
client = subject.new(server.base_uri, proxy: proxy.base_uri) do |c|
|
383
|
-
c.on_event { |event| event_sink << event }
|
384
|
-
end
|
385
|
-
|
386
|
-
with_client(client) do |client|
|
387
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
388
|
-
expect(proxy.request_count).to eq(1)
|
389
|
-
end
|
390
|
-
end
|
391
|
-
end
|
392
|
-
end
|
393
|
-
|
394
|
-
it "resets read timeout between events" do
|
395
|
-
event_body = simple_event_1_text
|
396
|
-
with_server do |server|
|
397
|
-
attempt = 0
|
398
|
-
server.setup_response("/") do |req,res|
|
399
|
-
attempt += 1
|
400
|
-
if attempt == 1
|
401
|
-
stream = send_stream_content(res, event_body, keep_open: true)
|
402
|
-
Thread.new do
|
403
|
-
2.times {
|
404
|
-
# write within timeout interval
|
405
|
-
sleep(0.75)
|
406
|
-
stream.write(event_body)
|
407
|
-
}
|
408
|
-
# cause timeout
|
409
|
-
sleep(1.25)
|
410
|
-
end
|
411
|
-
elsif attempt == 2
|
412
|
-
send_stream_content(res, event_body, keep_open: false)
|
413
|
-
end
|
414
|
-
end
|
415
|
-
|
416
|
-
event_sink = Queue.new
|
417
|
-
client = subject.new(server.base_uri, reconnect_time: reconnect_asap, read_timeout: 1) do |c|
|
418
|
-
c.on_event { |event| event_sink << event }
|
419
|
-
end
|
420
|
-
|
421
|
-
with_client(client) do |client|
|
422
|
-
4.times {
|
423
|
-
expect(event_sink.pop).to eq(simple_event_1)
|
424
|
-
}
|
425
|
-
expect(attempt).to eq 2
|
426
|
-
end
|
427
|
-
end
|
428
|
-
end
|
429
|
-
end
|
data/spec/event_parser_spec.rb
DELETED
@@ -1,100 +0,0 @@
|
|
1
|
-
require "ld-eventsource/impl/event_parser"
|
2
|
-
|
3
|
-
describe SSE::Impl::EventParser do
|
4
|
-
subject { SSE::Impl::EventParser }
|
5
|
-
|
6
|
-
it "parses an event with all fields" do
|
7
|
-
lines = [
|
8
|
-
"event: abc\r\n",
|
9
|
-
"data: def\r\n",
|
10
|
-
"id: 1\r\n",
|
11
|
-
"\r\n"
|
12
|
-
]
|
13
|
-
ep = subject.new(lines)
|
14
|
-
|
15
|
-
expected_event = SSE::StreamEvent.new(:abc, "def", "1")
|
16
|
-
output = ep.items.to_a
|
17
|
-
expect(output).to eq([ expected_event ])
|
18
|
-
end
|
19
|
-
|
20
|
-
it "parses an event with only data" do
|
21
|
-
lines = [
|
22
|
-
"data: def\r\n",
|
23
|
-
"\r\n"
|
24
|
-
]
|
25
|
-
ep = subject.new(lines)
|
26
|
-
|
27
|
-
expected_event = SSE::StreamEvent.new(:message, "def", nil)
|
28
|
-
output = ep.items.to_a
|
29
|
-
expect(output).to eq([ expected_event ])
|
30
|
-
end
|
31
|
-
|
32
|
-
it "parses an event with multi-line data" do
|
33
|
-
lines = [
|
34
|
-
"data: def\r\n",
|
35
|
-
"data: ghi\r\n",
|
36
|
-
"\r\n"
|
37
|
-
]
|
38
|
-
ep = subject.new(lines)
|
39
|
-
|
40
|
-
expected_event = SSE::StreamEvent.new(:message, "def\nghi", nil)
|
41
|
-
output = ep.items.to_a
|
42
|
-
expect(output).to eq([ expected_event ])
|
43
|
-
end
|
44
|
-
|
45
|
-
it "ignores comments" do
|
46
|
-
lines = [
|
47
|
-
":",
|
48
|
-
"data: def\r\n",
|
49
|
-
":",
|
50
|
-
"\r\n"
|
51
|
-
]
|
52
|
-
ep = subject.new(lines)
|
53
|
-
|
54
|
-
expected_event = SSE::StreamEvent.new(:message, "def", nil)
|
55
|
-
output = ep.items.to_a
|
56
|
-
expect(output).to eq([ expected_event ])
|
57
|
-
end
|
58
|
-
|
59
|
-
it "parses reconnect interval" do
|
60
|
-
lines = [
|
61
|
-
"retry: 2500\r\n",
|
62
|
-
"\r\n"
|
63
|
-
]
|
64
|
-
ep = subject.new(lines)
|
65
|
-
|
66
|
-
expected_item = SSE::Impl::SetRetryInterval.new(2500)
|
67
|
-
output = ep.items.to_a
|
68
|
-
expect(output).to eq([ expected_item ])
|
69
|
-
end
|
70
|
-
|
71
|
-
it "parses multiple events" do
|
72
|
-
lines = [
|
73
|
-
"event: abc\r\n",
|
74
|
-
"data: def\r\n",
|
75
|
-
"id: 1\r\n",
|
76
|
-
"\r\n",
|
77
|
-
"data: ghi\r\n",
|
78
|
-
"\r\n"
|
79
|
-
]
|
80
|
-
ep = subject.new(lines)
|
81
|
-
|
82
|
-
expected_event_1 = SSE::StreamEvent.new(:abc, "def", "1")
|
83
|
-
expected_event_2 = SSE::StreamEvent.new(:message, "ghi", nil)
|
84
|
-
output = ep.items.to_a
|
85
|
-
expect(output).to eq([ expected_event_1, expected_event_2 ])
|
86
|
-
end
|
87
|
-
|
88
|
-
it "ignores events with no data" do
|
89
|
-
lines = [
|
90
|
-
"event: nothing\r\n",
|
91
|
-
"\r\n",
|
92
|
-
"event: nada\r\n",
|
93
|
-
"\r\n"
|
94
|
-
]
|
95
|
-
ep = subject.new(lines)
|
96
|
-
|
97
|
-
output = ep.items.to_a
|
98
|
-
expect(output).to eq([])
|
99
|
-
end
|
100
|
-
end
|
data/spec/http_stub.rb
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
require "webrick"
|
2
|
-
require "webrick/httpproxy"
|
3
|
-
require "webrick/https"
|
4
|
-
|
5
|
-
class StubHTTPServer
|
6
|
-
attr_reader :port
|
7
|
-
|
8
|
-
def initialize
|
9
|
-
@port = 50000
|
10
|
-
begin
|
11
|
-
@server = create_server(@port)
|
12
|
-
rescue Errno::EADDRINUSE
|
13
|
-
@port += 1
|
14
|
-
retry
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def create_server(port)
|
19
|
-
WEBrick::HTTPServer.new(
|
20
|
-
BindAddress: '127.0.0.1',
|
21
|
-
Port: port,
|
22
|
-
AccessLog: [],
|
23
|
-
Logger: NullLogger.new
|
24
|
-
)
|
25
|
-
end
|
26
|
-
|
27
|
-
def start
|
28
|
-
Thread.new { @server.start }
|
29
|
-
end
|
30
|
-
|
31
|
-
def stop
|
32
|
-
@server.shutdown
|
33
|
-
end
|
34
|
-
|
35
|
-
def base_uri
|
36
|
-
URI("http://127.0.0.1:#{@port}")
|
37
|
-
end
|
38
|
-
|
39
|
-
def setup_response(uri_path, &action)
|
40
|
-
@server.mount_proc(uri_path, action)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
class StubProxyServer < StubHTTPServer
|
45
|
-
attr_reader :request_count
|
46
|
-
attr_accessor :connect_status
|
47
|
-
|
48
|
-
def initialize
|
49
|
-
super
|
50
|
-
@request_count = 0
|
51
|
-
end
|
52
|
-
|
53
|
-
def create_server(port)
|
54
|
-
WEBrick::HTTPProxyServer.new(
|
55
|
-
BindAddress: '127.0.0.1',
|
56
|
-
Port: port,
|
57
|
-
AccessLog: [],
|
58
|
-
Logger: NullLogger.new,
|
59
|
-
ProxyContentHandler: proc do |req,res|
|
60
|
-
if !@connect_status.nil?
|
61
|
-
res.status = @connect_status
|
62
|
-
end
|
63
|
-
@request_count += 1
|
64
|
-
end
|
65
|
-
)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
class NullLogger
|
70
|
-
def method_missing(*)
|
71
|
-
self
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def with_server(server = nil)
|
76
|
-
server = StubHTTPServer.new if server.nil?
|
77
|
-
begin
|
78
|
-
server.start
|
79
|
-
yield server
|
80
|
-
ensure
|
81
|
-
server.stop
|
82
|
-
end
|
83
|
-
end
|