ld-eventsource 1.0.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.
@@ -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