ld-eventsource 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module SSE
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,30 @@
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"
@@ -0,0 +1,346 @@
1
+ require "ld-eventsource"
2
+ require "socketry"
3
+ require "http_stub"
4
+
5
+ #
6
+ # End-to-end tests of the SSE client against a real server
7
+ #
8
+ describe SSE::Client do
9
+ subject { SSE::Client }
10
+
11
+ let(:simple_event_1) { SSE::StreamEvent.new(:go, "foo", "a")}
12
+ let(:simple_event_2) { SSE::StreamEvent.new(:stop, "bar", "b")}
13
+ let(:simple_event_1_text) { <<-EOT
14
+ event: go
15
+ data: foo
16
+ id: a
17
+
18
+ EOT
19
+ }
20
+ let(:simple_event_2_text) { <<-EOT
21
+ event: stop
22
+ data: bar
23
+ id: b
24
+
25
+ EOT
26
+ }
27
+ let(:reconnect_asap) { 0.01 }
28
+
29
+ def with_client(client)
30
+ begin
31
+ yield client
32
+ ensure
33
+ client.close
34
+ end
35
+ end
36
+
37
+ def send_stream_content(res, content, keep_open:)
38
+ res.content_type = "text/event-stream"
39
+ res.status = 200
40
+ res.chunked = true
41
+ rd, wr = IO.pipe
42
+ wr.write(content)
43
+ res.body = rd
44
+ if !keep_open
45
+ wr.close
46
+ end
47
+ wr
48
+ end
49
+
50
+ it "sends expected headers" do
51
+ with_server do |server|
52
+ requests = Queue.new
53
+ server.setup_response("/") do |req,res|
54
+ requests << req
55
+ send_stream_content(res, "", keep_open: true)
56
+ end
57
+
58
+ headers = { "Authorization" => "secret" }
59
+
60
+ with_client(subject.new(server.base_uri, headers: headers)) do |client|
61
+ received_req = requests.pop
62
+ expect(received_req.header).to eq({
63
+ "accept" => ["text/event-stream"],
64
+ "cache-control" => ["no-cache"],
65
+ "host" => ["127.0.0.1"],
66
+ "authorization" => ["secret"]
67
+ })
68
+ end
69
+ end
70
+ end
71
+
72
+ it "sends initial Last-Event-Id if specified" do
73
+ id = "xyz"
74
+ with_server do |server|
75
+ requests = Queue.new
76
+ server.setup_response("/") do |req,res|
77
+ requests << req
78
+ send_stream_content(res, "", keep_open: true)
79
+ end
80
+
81
+ headers = { "Authorization" => "secret" }
82
+
83
+ with_client(subject.new(server.base_uri, headers: headers, last_event_id: id)) do |client|
84
+ received_req = requests.pop
85
+ expect(received_req.header).to eq({
86
+ "accept" => ["text/event-stream"],
87
+ "cache-control" => ["no-cache"],
88
+ "host" => ["127.0.0.1"],
89
+ "authorization" => ["secret"],
90
+ "last-event-id" => [id]
91
+ })
92
+ end
93
+ end
94
+ end
95
+
96
+ it "receives messages" do
97
+ events_body = simple_event_1_text + simple_event_2_text
98
+ with_server do |server|
99
+ server.setup_response("/") do |req,res|
100
+ send_stream_content(res, events_body, keep_open: true)
101
+ end
102
+
103
+ event_sink = Queue.new
104
+ client = subject.new(server.base_uri) do |c|
105
+ c.on_event { |event| event_sink << event }
106
+ end
107
+
108
+ with_client(client) do |client|
109
+ expect(event_sink.pop).to eq(simple_event_1)
110
+ expect(event_sink.pop).to eq(simple_event_2)
111
+ end
112
+ end
113
+ end
114
+
115
+ it "reconnects after error response" do
116
+ events_body = simple_event_1_text
117
+ with_server do |server|
118
+ attempt = 0
119
+ server.setup_response("/") do |req,res|
120
+ attempt += 1
121
+ if attempt == 1
122
+ res.status = 500
123
+ res.body = "sorry"
124
+ res.keep_alive = false
125
+ else
126
+ send_stream_content(res, events_body, keep_open: true)
127
+ end
128
+ end
129
+
130
+ event_sink = Queue.new
131
+ error_sink = Queue.new
132
+ client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
133
+ c.on_event { |event| event_sink << event }
134
+ c.on_error { |error| error_sink << error }
135
+ end
136
+
137
+ with_client(client) do |client|
138
+ expect(event_sink.pop).to eq(simple_event_1)
139
+ expect(error_sink.pop).to eq(SSE::Errors::HTTPStatusError.new(500, "sorry"))
140
+ expect(attempt).to eq 2
141
+ end
142
+ end
143
+ end
144
+
145
+ it "reconnects after invalid content type" do
146
+ events_body = simple_event_1_text
147
+ with_server do |server|
148
+ attempt = 0
149
+ server.setup_response("/") do |req,res|
150
+ attempt += 1
151
+ if attempt == 1
152
+ res.status = 200
153
+ res.content_type = "text/plain"
154
+ res.body = "sorry"
155
+ res.keep_alive = false
156
+ else
157
+ send_stream_content(res, events_body, keep_open: true)
158
+ end
159
+ end
160
+
161
+ event_sink = Queue.new
162
+ error_sink = Queue.new
163
+ client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
164
+ c.on_event { |event| event_sink << event }
165
+ c.on_error { |error| error_sink << error }
166
+ end
167
+
168
+ with_client(client) do |client|
169
+ expect(event_sink.pop).to eq(simple_event_1)
170
+ expect(error_sink.pop).to eq(SSE::Errors::HTTPContentTypeError.new("text/plain"))
171
+ expect(attempt).to eq 2
172
+ end
173
+ end
174
+ end
175
+
176
+ it "reconnects after read timeout" do
177
+ events_body = simple_event_1_text
178
+ with_server do |server|
179
+ attempt = 0
180
+ server.setup_response("/") do |req,res|
181
+ attempt += 1
182
+ if attempt == 1
183
+ sleep(1)
184
+ end
185
+ send_stream_content(res, events_body, keep_open: true)
186
+ end
187
+
188
+ event_sink = Queue.new
189
+ client = subject.new(server.base_uri, reconnect_time: reconnect_asap, read_timeout: 0.25) do |c|
190
+ c.on_event { |event| event_sink << event }
191
+ end
192
+
193
+ with_client(client) do |client|
194
+ expect(event_sink.pop).to eq(simple_event_1)
195
+ expect(attempt).to eq 2
196
+ end
197
+ end
198
+ end
199
+
200
+ it "reconnects if stream returns EOF" do
201
+ with_server do |server|
202
+ attempt = 0
203
+ server.setup_response("/") do |req,res|
204
+ attempt += 1
205
+ send_stream_content(res, attempt == 1 ? simple_event_1_text : simple_event_2_text,
206
+ keep_open: attempt == 2)
207
+ end
208
+
209
+ event_sink = Queue.new
210
+ client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
211
+ c.on_event { |event| event_sink << event }
212
+ end
213
+
214
+ with_client(client) do |client|
215
+ expect(event_sink.pop).to eq(simple_event_1)
216
+ expect(event_sink.pop).to eq(simple_event_2)
217
+ expect(attempt).to eq 2
218
+ end
219
+ end
220
+ end
221
+
222
+ it "sends ID of last received event, if any, when reconnecting" do
223
+ with_server do |server|
224
+ requests = Queue.new
225
+ attempt = 0
226
+ server.setup_response("/") do |req,res|
227
+ requests << req
228
+ attempt += 1
229
+ send_stream_content(res, attempt == 1 ? simple_event_1_text : simple_event_2_text,
230
+ keep_open: attempt == 2)
231
+ end
232
+
233
+ event_sink = Queue.new
234
+ client = subject.new(server.base_uri, reconnect_time: reconnect_asap) do |c|
235
+ c.on_event { |event| event_sink << event }
236
+ end
237
+
238
+ with_client(client) do |client|
239
+ req1 = requests.pop
240
+ req2 = requests.pop
241
+ expect(req2.header["last-event-id"]).to eq([ simple_event_1.id ])
242
+ end
243
+ end
244
+ end
245
+
246
+ it "increases backoff delay if a failure happens within the reset threshold" do
247
+ request_times = []
248
+ max_requests = 5
249
+ initial_interval = 0.25
250
+
251
+ with_server do |server|
252
+ attempt = 0
253
+ server.setup_response("/") do |req,res|
254
+ request_times << Time.now
255
+ attempt += 1
256
+ send_stream_content(res, simple_event_1_text, keep_open: attempt == max_requests)
257
+ end
258
+
259
+ event_sink = Queue.new
260
+ client = subject.new(server.base_uri, reconnect_time: initial_interval) do |c|
261
+ c.on_event { |event| event_sink << event }
262
+ end
263
+
264
+ with_client(client) do |client|
265
+ last_interval = nil
266
+ max_requests.times do |i|
267
+ expect(event_sink.pop).to eq(simple_event_1)
268
+ if i > 0
269
+ interval = request_times[i] - request_times[i - 1]
270
+ minimum_expected_interval = initial_interval * (2 ** (i - 1)) / 2
271
+ expect(interval).to be >= minimum_expected_interval
272
+ last_interval = interval
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ it "resets backoff delay if a failure happens after the reset threshold" do
280
+ request_times = []
281
+ request_end_times = []
282
+ max_requests = 5
283
+ threshold = 0.3
284
+ initial_interval = 0.25
285
+
286
+ with_server do |server|
287
+ attempt = 0
288
+ server.setup_response("/") do |req,res|
289
+ request_times << Time.now
290
+ attempt += 1
291
+ stream = send_stream_content(res, simple_event_1_text, keep_open: true)
292
+ Thread.new do
293
+ sleep(threshold + 0.01)
294
+ stream.close
295
+ request_end_times << Time.now
296
+ end
297
+ end
298
+
299
+ event_sink = Queue.new
300
+ client = subject.new(server.base_uri, reconnect_time: initial_interval, reconnect_reset_interval: threshold) do |c|
301
+ c.on_event { |event| event_sink << event }
302
+ end
303
+
304
+ with_client(client) do |client|
305
+ last_interval = nil
306
+ max_requests.times do |i|
307
+ expect(event_sink.pop).to eq(simple_event_1)
308
+ if i > 0
309
+ interval = request_times[i] - request_end_times[i - 1]
310
+ expect(interval).to be <= initial_interval
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
316
+
317
+ it "can change initial reconnect delay based on directive from server" do
318
+ request_times = []
319
+ configured_interval = 1
320
+ retry_ms = 100
321
+
322
+ with_server do |server|
323
+ attempt = 0
324
+ server.setup_response("/") do |req,res|
325
+ request_times << Time.now
326
+ attempt += 1
327
+ if attempt == 1
328
+ send_stream_content(res, "retry: #{retry_ms}\n", keep_open: false)
329
+ else
330
+ send_stream_content(res, simple_event_1_text, keep_open: true)
331
+ end
332
+ end
333
+
334
+ event_sink = Queue.new
335
+ client = subject.new(server.base_uri, reconnect_time: configured_interval) do |c|
336
+ c.on_event { |event| event_sink << event }
337
+ end
338
+
339
+ with_client(client) do |client|
340
+ expect(event_sink.pop).to eq(simple_event_1)
341
+ interval = request_times[1] - request_times[0]
342
+ expect(interval).to be < ((retry_ms.to_f / 1000) + 0.1)
343
+ end
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,100 @@
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