roda-sse 0.1.0 → 0.2.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: 81bbd096f5155da3f8593b8e3d37771daedd543a9cf4816d5bab36f3192bb203
4
- data.tar.gz: 88366dcfba9fb10fcf5a810c540c3ae5b77c9f52989cdea5c03ea1b0a9aa9641
3
+ metadata.gz: 5e98e9c2879bb55ccadf0400d243d97ecff39fa4d6535a044cae739e3410878c
4
+ data.tar.gz: aa3d255a8cb4ecab1ddb9b5d71c916f6907c36529a934c38467ca57b4e3cb1a7
5
5
  SHA512:
6
- metadata.gz: 8375dd84ecfefe94dafb591d5edfb1e22a3413aa0f574b7c7a965b1adcf7ef62cd70397fb133a8f0e2468183edae44f82208742d357f45a43a69e53d9f352e6c
7
- data.tar.gz: 9bf26ed27a75bb355f1468baa668e0c3b569b9313d1468b60ac9aa6bbe7a2c142592011bacb7f931232e18a2bb90c932011b375a6b73e7d654a942ae0bb57210
6
+ metadata.gz: dee16a19a420769ed068a7bf977b4ef3a05b1d5425e1f9bf4ab2899697e0e13ac2255dc96af84178cb18ada3d6e85645ce91b5b7b4d91508213ecb422fc9a08b
7
+ data.tar.gz: 946654c542c416ae617e641e5dff7b3cb16d9fa8c5873946db293f5ff98b1d6043fdf9e71c6caa16acdf9f655affe0a14b38cf10fa89edeea0c774ebe1af02c8
data/README.md CHANGED
@@ -30,7 +30,5 @@ In your routing block, you can then use `r.sse` to stream with the correct heade
30
30
  r.sse do |stream|
31
31
  stream << "data: hello\n\n"
32
32
  stream << "data: world\n\n"
33
- ensure
34
- stream.close
35
33
  end
36
34
  ```
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'async'
4
+ require 'async/barrier'
5
+
3
6
  class Roda
4
7
  module RodaPlugins
5
8
  # Example:
@@ -11,22 +14,69 @@ class Roda
11
14
  # # GET /
12
15
  # r.sse do |stream|
13
16
  # stream.write "data: hola\n\n"
14
- # ensure
15
- # stream.close
16
17
  # end
17
18
  # end
18
19
  # end
19
20
  module SSE
21
+ class Stream
22
+ def initialize(&block)
23
+ @block = block
24
+ @barrier = Async::Barrier.new
25
+ end
26
+
27
+ def write(message)
28
+ data = message.to_s
29
+
30
+ @barrier.async do
31
+ @stream.write(data)
32
+ rescue Errno::ECONNRESET, Errno::EPIPE
33
+ @barrier.close
34
+ end
35
+
36
+ data.bytesize
37
+ end
38
+
39
+ def <<(message)
40
+ write(message)
41
+ self
42
+ end
43
+
44
+ def call(stream)
45
+ Sync do
46
+ @stream = stream
47
+ @block.call(stream)
48
+ ensure
49
+ close
50
+ end
51
+ end
52
+
53
+ def close
54
+ return if @closed
55
+
56
+ @barrier.stop
57
+ @stream&.close
58
+ @closed = true
59
+ end
60
+
61
+ def closed? = @closed
62
+ end
63
+
20
64
  module RequestMethods
21
65
  def sse(&block)
22
- response['Content-Type'] = 'text/event-stream'
23
- response['Cache-Control'] = 'no-cache'
66
+ get do
67
+ response['Content-Type'] = 'text/event-stream'
68
+ response['Cache-Control'] = 'no-cache'
24
69
 
25
- always do
26
- halt response.finish_with_body(block)
70
+ halt response.finish_with_body(Stream.new(&block))
27
71
  end
28
72
  end
29
73
  end
74
+
75
+ module InstanceMethods
76
+ def last_event_id
77
+ env['HTTP_LAST_EVENT_ID']
78
+ end
79
+ end
30
80
  end
31
81
 
32
82
  register_plugin(:sse, SSE)
@@ -12,9 +12,8 @@ class App < Roda
12
12
  route do |r|
13
13
  r.root do
14
14
  r.sse do |stream|
15
+ last_event_id
15
16
  stream.write "data: hola\n\n"
16
- ensure
17
- stream.close
18
17
  end
19
18
  end
20
19
  end
@@ -23,34 +22,66 @@ end
23
22
  def app = App.freeze.app
24
23
 
25
24
  describe 'roda-sse plugin' do
26
- include Rack::Test::Methods
27
-
28
25
  prove_it!
29
26
 
30
- it 'responds 200 OK' do
31
- get '/'
27
+ describe 'roda app' do
28
+ include Rack::Test::Methods
32
29
 
33
- assert last_response.ok?
34
- end
30
+ it 'responds 200 OK' do
31
+ get '/'
32
+
33
+ assert last_response.ok?
34
+ end
35
+
36
+ it 'does not respond to PUT' do
37
+ post '/'
35
38
 
36
- it 'has SSE headers' do
37
- get '/'
39
+ refute last_response.ok?
40
+ end
41
+
42
+ it 'has SSE headers' do
43
+ get '/'
44
+
45
+ headers = {'content-type' => 'text/event-stream', 'cache-control' => 'no-cache'}
46
+ assert_equal headers, last_response.headers
47
+ end
38
48
 
39
- headers = {'content-type' => 'text/event-stream', 'cache-control' => 'no-cache'}
40
- assert_equal headers, last_response.headers
49
+ it 'streams the body' do
50
+ get '/'
51
+
52
+ stream = Minitest::Mock.new
53
+ stream.expect(:write, nil, ["data: hola\n\n"])
54
+ stream.expect(:close, nil)
55
+ response_body = last_response.instance_variable_get(:@body)
56
+ assert_instance_of Roda::RodaPlugins::SSE::Stream, response_body
57
+
58
+ response_body.call(stream)
59
+
60
+ stream.verify
61
+ end
41
62
  end
42
63
 
43
- it 'streams the body' do
44
- get '/'
64
+ describe 'stream class' do
65
+ before do
66
+ @stream = Roda::RodaPlugins::SSE::Stream.new do |stream|
67
+ stream << 42
68
+ stream.write(43)
69
+ end
70
+ end
45
71
 
46
- stream = Minitest::Mock.new
47
- stream.expect(:write, nil, ["data: hola\n\n"])
48
- stream.expect(:close, nil)
49
- response_body = last_response.instance_variable_get(:@body)
50
- assert_instance_of Proc, response_body
72
+ it 'opens' do
73
+ refute @stream.closed?
74
+ end
51
75
 
52
- response_body.call(stream)
76
+ it 'streams and closes' do
77
+ stream = Minitest::Mock.new
78
+ stream.expect(:<<, nil, [42])
79
+ stream.expect(:write, nil, [43])
80
+ stream.expect(:close, nil)
81
+ @stream.call(stream)
82
+ assert_instance_of Roda::RodaPlugins::SSE::Stream, @stream
53
83
 
54
- stream.verify
84
+ stream.verify
85
+ end
55
86
  end
56
87
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda-sse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shannon Skipper
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-10-18 00:00:00.000000000 Z
10
+ date: 2024-11-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: roda
@@ -15,14 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '3.0'
18
+ version: '3.85'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '3.0'
25
+ version: '3.85'
26
+ - !ruby/object:Gem::Dependency
27
+ name: async
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.18'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.18'
26
40
  description: The roda-sse gem integrates simple SSE streaming into the roda web toolkit.
27
41
  email: shannonskipper@gmail.com
28
42
  executables: []