ld-celluloid-eventsource 0.4.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f33aac634f96a65252e98eaca774d9f5df2a12e6
4
+ data.tar.gz: 77f105c227ead24d0d477345f1a30bb0093eb661
5
+ SHA512:
6
+ metadata.gz: dd143c0275f774054aaf7a1246f87ca462fd03202d011984f57735c8f7480eb1aa3aee77a40816e0acd1fc1c0e8adac9d18162eb9e27ececcad74651724393c8
7
+ data.tar.gz: 2896213a7d5a0155df43ca9aab8b097d4a45bdae971c45fc4befaa92113929791dd5e1e4da90557e9518a4310a9a702bf464bea6c169b240f82e45bf8693857a
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .ruby-version
7
+ .ruby-gemset
8
+ examples/
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
21
+ .idea
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+
3
+ before_install: gem update bundler
4
+
5
+ rvm:
6
+ - "2.0.0"
7
+ - "2.1.0"
8
+ - "2.2.2"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in celluloid-eventsource.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Leo Correa
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Celluloid::Eventsource
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/celluloid-eventsource.png)](http://badge.fury.io/rb/celluloid-eventsource)
4
+ [![Code Climate](https://codeclimate.com/github/Tonkpils/celluloid-eventsource.png)](https://codeclimate.com/github/Tonkpils/celluloid-eventsource)
5
+ [![Build Status](https://travis-ci.org/Tonkpils/celluloid-eventsource.svg?branch=master)](https://travis-ci.org/Tonkpils/celluloid-eventsource)
6
+
7
+ An EventSource client based off Celluloid::IO.
8
+
9
+ Specification based on [EventSource](http://www.w3.org/TR/2012/CR-eventsource-20121211/)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'celluloid-eventsource'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install celluloid-eventsource
24
+
25
+ then somewhere in your project:
26
+
27
+ require 'celluloid/eventsource'
28
+
29
+ ## Usage
30
+
31
+ Initializing a new `Celluloid::EventSource` object will create the connection:
32
+
33
+ ```ruby
34
+ es = Celluloid::EventSource.new("http://example.com/")
35
+ ```
36
+
37
+ Messages can be received on events such as `on_open`, `on_message` and `on_error`.
38
+
39
+ These can be assigned at initialize time
40
+
41
+ ```ruby
42
+ es = Celluloid::EventSource.new("http://example.com/") do |conn|
43
+ conn.on_open do
44
+ puts "Connection was made"
45
+ end
46
+
47
+ conn.on_message do |event|
48
+ puts "Message: #{event.data}"
49
+ end
50
+
51
+ conn.on_error do |message|
52
+ puts "Response status #{message[:status_code]}, Response body #{message[:body]}"
53
+ end
54
+
55
+ conn.on(:time) do |event|
56
+ puts "The time is #{event.data}"
57
+ end
58
+ end
59
+ ```
60
+
61
+ To close the connection `#close` will shut the socket connection but keep the actor alive.
62
+
63
+ ### Event Handlers
64
+
65
+ Event handlers should be added when initializing the eventsource.
66
+
67
+ **Warning**
68
+ To change event handlers after initializing there is a [Gotcha](https://github.com/celluloid/celluloid/wiki/Gotchas).
69
+ Celluloid sends messages to actors through thread-safe proxies.
70
+
71
+ To get around this, use `wrapped_object` to set the handler on the actor but be aware of the concequences.
72
+
73
+ ```ruby
74
+ es.wrapped_object.on_messsage { |message| puts "Different #{message}" }
75
+ ```
76
+
77
+ This same concept applies for changing the `url` of the eventsource.
78
+
79
+ ## Contributing
80
+
81
+ 1. Fork it
82
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
83
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
84
+ 4. Push to the branch (`git push origin my-new-feature`)
85
+ 5. Create new Pull Request
86
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new :spec
5
+
6
+ task :default => :spec
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'celluloid/eventsource/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ld-celluloid-eventsource"
8
+ spec.version = Celluloid::EventSource::VERSION
9
+ spec.authors = ["Leo Correa"]
10
+ spec.email = ["lcorr005@gmail.com"]
11
+ spec.description = %q{Celluloid::IO based library to consume Server-Sent Events. This library was forked from https://github.com/Tonkpils/celluloid-eventsource}
12
+ spec.summary = %q{ld-celluloid-eventsource is a gem to consume SSE streaming API.}
13
+ spec.homepage = "https://github.com/launchdarkly/celluloid-eventsource"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'celluloid-io', '~> 0.17.3'
22
+ spec.add_dependency 'http_parser.rb', '~> 0.6.0'
23
+
24
+ spec.add_development_dependency 'atomic', '~> 1.1'
25
+ spec.add_development_dependency "rspec", '~> 3.0'
26
+ spec.add_development_dependency "bundler", "~> 1.7"
27
+ spec.add_development_dependency "rake", '~> 10.1'
28
+ spec.add_development_dependency "pry", '~> 0.9'
29
+ end
@@ -0,0 +1,61 @@
1
+ require 'http/parser'
2
+
3
+ module Celluloid
4
+
5
+ class EventSource
6
+
7
+ class ResponseParser
8
+ extend Forwardable
9
+
10
+ attr_reader :headers
11
+
12
+ delegate [:status_code, :<<] => :@parser
13
+
14
+ def initialize
15
+ @parser = Http::Parser.new(self)
16
+ @headers = nil
17
+ @chunk = ""
18
+ end
19
+
20
+ def headers?
21
+ !!@headers
22
+ end
23
+
24
+ def status
25
+ @parser.status_code
26
+ end
27
+
28
+ def on_headers_complete(headers)
29
+ @headers = canonical_headers(headers)
30
+ end
31
+
32
+ def on_body(chunk)
33
+ @chunk << chunk
34
+ end
35
+
36
+ def chunk
37
+ chunk = @chunk
38
+ unless chunk.empty?
39
+ @chunk = ""
40
+ end
41
+
42
+ chunk.to_s
43
+ end
44
+
45
+ private
46
+
47
+ def canonical_headers(headers)
48
+ headers.each_with_object({}) do |(key, value), canonicalized_headers|
49
+ name = canonicalize_header(key)
50
+ canonicalized_headers[name] = value
51
+ end
52
+ end
53
+
54
+ def canonicalize_header(name)
55
+ name.gsub('_', '-').split("-").map(&:capitalize).join("-")
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,5 @@
1
+ module Celluloid
2
+ class EventSource
3
+ VERSION = "0.4.0"
4
+ end
5
+ end
@@ -0,0 +1,232 @@
1
+ require 'celluloid/current'
2
+ require "celluloid/eventsource/version"
3
+ require 'celluloid/io'
4
+ require 'celluloid/eventsource/response_parser'
5
+ require 'uri'
6
+
7
+ module Celluloid
8
+ class EventSource
9
+ include Celluloid::IO
10
+
11
+ attr_reader :url, :with_credentials
12
+ attr_reader :ready_state
13
+
14
+ CONNECTING = 0
15
+ OPEN = 1
16
+ CLOSED = 2
17
+
18
+ execute_block_on_receiver :initialize
19
+
20
+ def initialize(uri, options = {})
21
+ self.url = uri
22
+ options = options.dup
23
+ @ready_state = CONNECTING
24
+ @with_credentials = options.delete(:with_credentials) { false }
25
+ @headers = default_request_headers.merge(options.fetch(:headers, {}))
26
+
27
+ @event_type_buffer = ""
28
+ @last_event_id_buffer = ""
29
+ @data_buffer = ""
30
+
31
+ @last_event_id = String.new
32
+
33
+ @reconnect_timeout = 10
34
+ @on = { open: ->{}, message: ->(_) {}, error: ->(_) {} }
35
+ @parser = ResponseParser.new
36
+
37
+ @chunked = false
38
+
39
+ yield self if block_given?
40
+
41
+ async.listen
42
+ end
43
+
44
+ def url=(uri)
45
+ @url = URI(uri)
46
+ end
47
+
48
+ def connected?
49
+ ready_state == OPEN
50
+ end
51
+
52
+ def closed?
53
+ ready_state == CLOSED
54
+ end
55
+
56
+ def listen
57
+ establish_connection
58
+
59
+ chunked? ? process_chunked_stream : process_stream
60
+ rescue
61
+ after(1){ listen }
62
+ end
63
+
64
+ def close
65
+ @socket.close if @socket
66
+ @ready_state = CLOSED
67
+ end
68
+
69
+ def on(event_name, &action)
70
+ @on[event_name.to_sym] = action
71
+ end
72
+
73
+ def on_open(&action)
74
+ @on[:open] = action
75
+ end
76
+
77
+ def on_message(&action)
78
+ @on[:message] = action
79
+ end
80
+
81
+ def on_error(&action)
82
+ @on[:error] = action
83
+ end
84
+
85
+ private
86
+
87
+ MessageEvent = Struct.new(:type, :data, :last_event_id)
88
+
89
+ def ssl?
90
+ url.scheme == 'https'
91
+ end
92
+
93
+ def establish_connection
94
+ @socket = Celluloid::IO::TCPSocket.new(@url.host, @url.port)
95
+
96
+ if ssl?
97
+ @socket = Celluloid::IO::SSLSocket.new(@socket)
98
+ @socket.connect
99
+ end
100
+
101
+ @socket.write(request_string)
102
+
103
+ until @parser.headers?
104
+ @parser << @socket.readline
105
+ end
106
+
107
+ if @parser.status_code != 200
108
+ until @socket.eof?
109
+ @parser << @socket.readline
110
+ end
111
+ close
112
+ @on[:error].call({status_code: @parser.status_code, body: @parser.chunk})
113
+ return
114
+ end
115
+
116
+ handle_headers(@parser.headers)
117
+ end
118
+
119
+ def default_request_headers
120
+ {
121
+ 'Accept' => 'text/event-stream',
122
+ 'Cache-Control' => 'no-cache',
123
+ 'Host' => url.host
124
+ }
125
+ end
126
+
127
+ def clear_buffers!
128
+ @data_buffer = ""
129
+ @event_type_buffer = ""
130
+ end
131
+
132
+ def dispatch_event(event)
133
+ unless closed?
134
+ @on[event.type] && @on[event.type].call(event)
135
+ end
136
+ end
137
+
138
+ def chunked?
139
+ @chunked
140
+ end
141
+
142
+ def process_chunked_stream
143
+ until closed? || @socket.eof?
144
+ handle_chunked_stream
145
+ end
146
+ end
147
+
148
+ def process_stream
149
+ until closed? || @socket.eof?
150
+ line = @socket.readline
151
+ line.strip.empty? ? process_event : parse_line(line)
152
+ end
153
+ end
154
+
155
+ def handle_chunked_stream
156
+ chunk_header = @socket.readline
157
+ bytes_to_read = chunk_header.to_i(16)
158
+ bytes_read = 0
159
+ while bytes_read < bytes_to_read do
160
+ line = @socket.readline
161
+ bytes_read += line.size
162
+
163
+ line.strip.empty? ? process_event : parse_line(line)
164
+ end
165
+
166
+ if !line.nil? && line.strip.empty?
167
+ process_event
168
+ end
169
+ end
170
+
171
+ def parse_line(line)
172
+ case line
173
+ when /^:.*$/
174
+ when /^(\w+): ?(.*)$/
175
+ process_field($1, $2)
176
+ else
177
+ if chunked? && !@data_buffer.empty?
178
+ @data_buffer.rstrip!
179
+ process_field("data", line.rstrip)
180
+ end
181
+ end
182
+ end
183
+
184
+ def process_event
185
+ @last_event_id = @last_event_id_buffer
186
+
187
+ return if @data_buffer.empty?
188
+
189
+ @data_buffer.chomp!("\n") if @data_buffer.end_with?("\n")
190
+ event = MessageEvent.new(:message, @data_buffer, @last_event_id)
191
+ event.type = @event_type_buffer.to_sym unless @event_type_buffer.empty?
192
+
193
+ dispatch_event(event)
194
+ ensure
195
+ clear_buffers!
196
+ end
197
+
198
+ def process_field(field_name, field_value)
199
+ case field_name
200
+ when "event"
201
+ @event_type_buffer = field_value
202
+ when "data"
203
+ @data_buffer << field_value.concat("\n")
204
+ when "id"
205
+ @last_event_id_buffer = field_value
206
+ when "retry"
207
+ if /^(?<num>\d+)$/ =~ field_value
208
+ @reconnect_timeout = num.to_i
209
+ end
210
+ end
211
+ end
212
+
213
+ def handle_headers(headers)
214
+ if headers['Content-Type'].include?("text/event-stream")
215
+ @chunked = !headers["Transfer-Encoding"].nil? && headers["Transfer-Encoding"].include?("chunked")
216
+ @ready_state = OPEN
217
+ @on[:open].call
218
+ else
219
+ close
220
+ @on[:error].call({status_code: @parser.status_code, body: "Invalid Content-Type #{headers['Content-Type']}. Expected text/event-stream"})
221
+ end
222
+ end
223
+
224
+ def request_string
225
+ headers = @headers.map { |k, v| "#{k}: #{v}" }
226
+
227
+ ["GET #{url.request_uri} HTTP/1.1", headers].flatten.join("\r\n").concat("\r\n\r\n")
228
+ end
229
+
230
+ end
231
+
232
+ end
data/log/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.log
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Celluloid::EventSource::ResponseParser do
4
+
5
+ let(:success_headers) {<<-eos
6
+ HTTP/1.1 200 OK
7
+ Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
8
+ Content-Type: text/html; charset=UTF-8
9
+ Content-Length: 131
10
+ X_CASE_HEADER: foo
11
+ X_Mixed-Case: bar
12
+ x-lowcase-header: hello
13
+ eos
14
+ }
15
+
16
+ let(:error_headers) {<<-eos
17
+ HTTP/1.1 400 OK
18
+ Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
19
+ Content-Type: text/html; charset=UTF-8
20
+ Content-Length: 131
21
+ eos
22
+ }
23
+
24
+ let(:response_string) { "#{success_headers}\n\n{'hello' : 'world'}\n"}
25
+
26
+ let(:parser) { subject }
27
+
28
+
29
+ def streamed(string)
30
+ stream = StringIO.new(string)
31
+ stream.each do |line|
32
+ yield line
33
+ end
34
+ end
35
+
36
+ it 'parses a complete http response' do
37
+ streamed(response_string) do |line|
38
+ parser << line
39
+ end
40
+
41
+ expect(parser.status).to eq(200)
42
+ expect(parser.headers?).to be_truthy
43
+ expect(parser.headers['Content-Type']).to eq('text/html; charset=UTF-8')
44
+ expect(parser.headers['Content-Length']).to eq("131")
45
+ end
46
+
47
+ it 'waits until the entire request is found' do
48
+ streamed(success_headers) do |line|
49
+ parser << line
50
+ end
51
+
52
+ expect(parser.status).to eq(200)
53
+ expect(parser.headers?).to be_falsey
54
+ expect(parser.headers).to be_nil
55
+ end
56
+
57
+ it 'makes response headers canonicalized' do
58
+ streamed(response_string) { |line| parser << line }
59
+ expected_headers = {
60
+ 'X-Mixed-Case' => 'bar', 'Content-Type' => 'text/html; charset=UTF-8', 'Content-Length' => "131",
61
+ 'X-Case-Header' => 'foo', 'X-Lowcase-Header' => "hello"
62
+ }
63
+ expect(parser.headers).to include(expected_headers)
64
+ end
65
+
66
+ end
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+ require 'support/dummy_server'
3
+
4
+
5
+ # See: https://html.spec.whatwg.org/multipage/comms.html#event-stream-interpretation
6
+ RSpec.describe Celluloid::EventSource do
7
+
8
+ let!(:chunk_size) { DummyServer::CHUNK_SIZE }
9
+
10
+ def dummy
11
+ @dummy ||= DummyServer.new
12
+ end
13
+
14
+ before(:all) do
15
+ dummy.listen(DummyServer::CONFIG[:BindAddress], DummyServer::CONFIG[:Port])
16
+ Thread.new { dummy.start }
17
+ end
18
+
19
+ after(:all) do
20
+ dummy.shutdown
21
+ end
22
+
23
+ describe '#initialize' do
24
+ let(:url) { "example.com" }
25
+
26
+ it 'runs asynchronously' do
27
+ ces = double(Celluloid::EventSource)
28
+ expect_any_instance_of(Celluloid::EventSource).to receive_message_chain(:async, :listen).and_return(ces)
29
+
30
+ Celluloid::EventSource.new("http://#{url}")
31
+ end
32
+
33
+ it 'allows customizing headers' do
34
+ auth_header = { "Authorization" => "Basic aGVsbG86dzBybGQh" }
35
+
36
+ allow_any_instance_of(Celluloid::EventSource).to receive_message_chain(:async, :listen)
37
+ es = Celluloid::EventSource.new("http://#{url}", :headers => auth_header)
38
+
39
+ headers = es.instance_variable_get('@headers')
40
+ expect(headers['Authorization']).to eq(auth_header["Authorization"])
41
+ end
42
+ end
43
+
44
+ context 'callbacks' do
45
+
46
+ let(:future) { Celluloid::Future.new }
47
+ let(:value_class) { Class.new(Struct.new(:value)) }
48
+
49
+ describe '#on_open' do
50
+
51
+ it 'the client has an opened connection' do
52
+
53
+ Celluloid::EventSource.new(dummy.endpoint) do |conn|
54
+ conn.on_open do
55
+ future.signal(value_class.new({ called: true, state: conn.ready_state }))
56
+ conn.close
57
+ end
58
+ end
59
+
60
+ expect(future.value).to eq({ called: true, state: Celluloid::EventSource::OPEN })
61
+ end
62
+ end
63
+
64
+ describe '#on_error' do
65
+
66
+ it 'receives response body through error event' do
67
+
68
+ Celluloid::EventSource.new("#{dummy.endpoint}/error") do |conn|
69
+ conn.on_error do |error|
70
+ future.signal(value_class.new({ msg: error, state: conn.ready_state }))
71
+ end
72
+ end
73
+
74
+ expect(future.value).to eq({ msg: { status_code: 400, body: '{"msg": "blop"}' },
75
+ state: Celluloid::EventSource::CLOSED })
76
+ end
77
+ end
78
+
79
+ describe '#on_message' do
80
+
81
+ it 'receives data through message event' do
82
+
83
+ Celluloid::EventSource.new(dummy.endpoint) do |conn|
84
+ conn.on_message do |message|
85
+ if '3' == message.last_event_id
86
+ future.signal(value_class.new({ msg: message, state: conn.ready_state }))
87
+ conn.close
88
+ end
89
+ end
90
+ end
91
+
92
+ payload = future.value
93
+ expect(payload[:msg]).to be_a(Celluloid::EventSource::MessageEvent)
94
+ expect(payload[:msg].type).to eq(:message)
95
+ expect(payload[:msg].last_event_id).to eq('3')
96
+ expect(payload[:state]).to eq(Celluloid::EventSource::OPEN)
97
+ end
98
+
99
+ it 'ignores lines starting with ":"' do
100
+ Celluloid::EventSource.new("#{dummy.endpoint}/ping") do |conn|
101
+ conn.on_message do |message|
102
+ future.signal(value_class.new({ msg: message, state: conn.ready_state }))
103
+ conn.close
104
+ end
105
+ end
106
+
107
+ expect(future.value(3)[:msg].data).to eq('pong')
108
+ end
109
+
110
+ it "aggregates events properly" do
111
+ Celluloid::EventSource.new("#{dummy.endpoint}/continuous") do |conn|
112
+ conn.on_message do |message|
113
+ future.signal(value_class.new({ msg: message, state: conn.ready_state }))
114
+ conn.close
115
+ end
116
+ end
117
+ expect(future.value(3)[:msg].data).to eq("YHOO\n+2\n10")
118
+ end
119
+
120
+ context "with chunked streams" do
121
+ it "properly parses chunked encoding" do
122
+ Celluloid::EventSource.new("#{dummy.endpoint}/chunk") do |conn|
123
+ conn.on_message do |message|
124
+ future.signal(value_class.new({ msg: message, state: conn.ready_state }))
125
+ conn.close
126
+ end
127
+ end
128
+ expect(future.value(3)[:msg].data).to eq("f" * (chunk_size + 25))
129
+ end
130
+
131
+ it "parses multiple continuous chunks" do
132
+ Celluloid::EventSource.new("#{dummy.endpoint}/continuous_chunks") do |conn|
133
+ conn.on_message do |message|
134
+ future.signal(value_class.new({ msg: message, state: conn.ready_state }))
135
+ conn.close
136
+ end
137
+ end
138
+
139
+ data = "o" * chunk_size + "\n" + "m" * chunk_size + "\n" + "g" * chunk_size
140
+ expect(future.value(3)[:msg].data).to eq(data)
141
+ end
142
+
143
+ it "parses multiple chunks" do
144
+ Celluloid::EventSource.new("#{dummy.endpoint}/multiple_chunks") do |conn|
145
+ conn.on_message do |message|
146
+ future.signal(value_class.new({ msg: message, state: conn.ready_state }))
147
+ conn.close
148
+ end
149
+ end
150
+
151
+ expect(future.value(3)[:msg].data).to eq({test: "long_chunk", another_chunk: "a" * chunk_size, chunks: "f" * chunk_size }.to_json)
152
+ end
153
+ end
154
+ end
155
+
156
+ describe '#on' do
157
+ let(:custom) { :custom_event }
158
+
159
+ it 'receives custom events and handles them' do
160
+
161
+ Celluloid::EventSource.new("#{dummy.endpoint}/#{custom}") do |conn|
162
+ conn.on(custom) do |message|
163
+ future.signal(value_class.new({ msg: message, state: conn.ready_state }))
164
+ conn.close
165
+ end
166
+ end
167
+
168
+ payload = future.value
169
+ expect(payload[:msg]).to be_a(Celluloid::EventSource::MessageEvent)
170
+ expect(payload[:msg].type).to eq(custom)
171
+ expect(payload[:msg].last_event_id).to eq('1')
172
+ expect(payload[:state]).to eq(Celluloid::EventSource::OPEN)
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'celluloid/current'
5
+ require 'celluloid/test'
6
+ require 'celluloid/eventsource'
7
+
8
+ logfile = File.open(File.expand_path("../../log/test.log", __FILE__), 'a')
9
+ logfile.sync = true
10
+
11
+ Celluloid.logger = Logger.new(logfile)
12
+
13
+ RSpec.configure do |config|
14
+ config.expose_dsl_globally = false
15
+
16
+ config.around(:each) do |ex|
17
+ Celluloid.boot
18
+ ex.run
19
+ Celluloid.shutdown
20
+ end
21
+
22
+ # Run specs in random order to surface order dependencies. If you find an
23
+ # order dependency and want to debug it, you can fix the order by providing
24
+ # the seed, which is printed after each run.
25
+ # --seed 1234
26
+ config.order = :random
27
+ end
@@ -0,0 +1,5 @@
1
+ module BlackHole
2
+ def self.method_missing(*)
3
+ self
4
+ end
5
+ end
@@ -0,0 +1,100 @@
1
+ require 'webrick'
2
+ require 'atomic'
3
+ require 'json'
4
+
5
+ require 'support/black_hole'
6
+
7
+ class DummyServer < WEBrick::HTTPServer
8
+ CHUNK_SIZE = 100
9
+ CONFIG = {
10
+ :BindAddress => '127.0.0.1',
11
+ :Port => 5000,
12
+ :AccessLog => BlackHole,
13
+ :Logger => BlackHole,
14
+ :DoNotListen => true,
15
+ :OutputBufferSize => CHUNK_SIZE
16
+ }.freeze
17
+
18
+ def initialize(options = {})
19
+ super(CONFIG)
20
+ mount('/', SSETestServlet)
21
+ mount('/error', ErrorServlet)
22
+ end
23
+
24
+ def endpoint
25
+ "#{scheme}://#{addr}:#{port}"
26
+ end
27
+
28
+ def addr
29
+ config[:BindAddress]
30
+ end
31
+
32
+ def port
33
+ config[:Port]
34
+ end
35
+
36
+ def scheme
37
+ 'http'
38
+ end
39
+
40
+ # Simple server that broadcasts Time.now
41
+ class SSETestServlet < WEBrick::HTTPServlet::AbstractServlet
42
+
43
+ def initialize(*args)
44
+ @event_id = Atomic.new(0)
45
+ super
46
+ end
47
+
48
+ def do_GET(req, res)
49
+ event = String(Array(req.path.match(/\/?(\w+)/i)).pop).to_sym
50
+ res.content_type = 'text/event-stream; charset=utf-8'
51
+ res['Cache-Control'] = 'no-cache'
52
+ r,w = IO.pipe
53
+ res.body = r
54
+ res.chunked = true
55
+ t = Thread.new do
56
+ begin
57
+ w << "retry: 1000\n"
58
+ case event
59
+ when :continuous_chunks
60
+ data = "data: %s\ndata: %s\ndata: %s\n\n" % ["o" * CHUNK_SIZE, "m" * CHUNK_SIZE, "g" * CHUNK_SIZE]
61
+ w << data
62
+ when :multiple_chunks
63
+ data = {test: "long_chunk", another_chunk: "a" * CHUNK_SIZE, chunks: "f" * CHUNK_SIZE}.to_json
64
+ w << "data: #{data}\n\n"
65
+ when :continuous
66
+ w << "data: YHOO\ndata: +2\ndata: 10\n\n"
67
+ when :chunk
68
+ data = "f" * (CHUNK_SIZE + 25)
69
+ w << "data: %s\n\n" % data
70
+ when :ping
71
+ w << ": ignore this line\n"
72
+ w << "event: \ndata: pong\n\n" # easy way to know a 'ping' has been sent
73
+ else
74
+ 42.times do
75
+ w << "id: %s\nevent: %s\ndata: %s\n\n" % [ @event_id.update { |v| v + 1 },
76
+ event,
77
+ Time.now ]
78
+ end
79
+ w << "event: %s\ndata: %s\n\n" % %w(end end)
80
+ end
81
+ rescue => ex
82
+ puts $!.inspect, $@ unless ex.is_a?(Errno::EPIPE)
83
+ ensure
84
+ w.close
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ class ErrorServlet < WEBrick::HTTPServlet::AbstractServlet
91
+ def do_GET(req, res)
92
+ res.content_type = 'application/json; charset=utf-8'
93
+ res.status = 400
94
+ res.keep_alive = false # true by default
95
+ res.body = '{"msg": "blop"}'
96
+ end
97
+ end
98
+ end
99
+
100
+
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ld-celluloid-eventsource
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Leo Correa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: celluloid-io
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.17.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.17.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: http_parser.rb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 0.6.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: atomic
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '10.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '10.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ description: Celluloid::IO based library to consume Server-Sent Events. This library
112
+ was forked from https://github.com/Tonkpils/celluloid-eventsource
113
+ email:
114
+ - lcorr005@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - .gitignore
120
+ - .rspec
121
+ - .travis.yml
122
+ - Gemfile
123
+ - LICENSE
124
+ - README.md
125
+ - Rakefile
126
+ - ld-celluloid-eventsource.gemspec
127
+ - lib/celluloid/eventsource.rb
128
+ - lib/celluloid/eventsource/response_parser.rb
129
+ - lib/celluloid/eventsource/version.rb
130
+ - log/.gitignore
131
+ - spec/celluloid/eventsource/response_parser_spec.rb
132
+ - spec/celluloid/eventsource_spec.rb
133
+ - spec/spec_helper.rb
134
+ - spec/support/black_hole.rb
135
+ - spec/support/dummy_server.rb
136
+ homepage: https://github.com/launchdarkly/celluloid-eventsource
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - '>='
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.5.0
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: ld-celluloid-eventsource is a gem to consume SSE streaming API.
160
+ test_files:
161
+ - spec/celluloid/eventsource/response_parser_spec.rb
162
+ - spec/celluloid/eventsource_spec.rb
163
+ - spec/spec_helper.rb
164
+ - spec/support/black_hole.rb
165
+ - spec/support/dummy_server.rb