ld-celluloid-eventsource 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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