redisse 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ require_relative '../lib/redisse/server_sent_events'
3
+
4
+ describe Redisse::ServerSentEvents do
5
+
6
+ include described_class
7
+
8
+ it "outputs a basic Server-Sent Event" do
9
+ expect(server_sent_event('foobar')).
10
+ to match(/\Adata: ?foobar\n\n\z/)
11
+ end
12
+
13
+ it "outputs the empty string with nil data" do
14
+ expect(server_sent_event(nil)).
15
+ to match(/\Adata: ?\n\n\z/)
16
+ end
17
+
18
+ it "uses data as a String" do
19
+ object = Object.new
20
+ def object.to_s
21
+ "data"
22
+ end
23
+ expect(server_sent_event(object)).
24
+ to match(/\Adata: ?data\n\n\z/)
25
+ end
26
+
27
+ it "outputs a Server-Sent Event with type" do
28
+ event = server_sent_event('foo', type: :event_type_foo)
29
+ expect(event).
30
+ to match(/^event: ?event_type_foo$/)
31
+ expect(event).to end_with("\n\n")
32
+ end
33
+
34
+ it "outputs a Server-Sent Event with id" do
35
+ event = server_sent_event('foo', id: 12)
36
+ expect(event).
37
+ to match(/^id: ?12$/)
38
+ expect(event).to end_with("\n\n")
39
+ end
40
+
41
+ it "outputs a Server-Sent Event with retry" do
42
+ event = server_sent_event('foo', retry: 500)
43
+ expect(event).
44
+ to match(/^retry: ?500$/)
45
+ expect(event).to end_with("\n\n")
46
+ end
47
+
48
+ it "outputs a Server-Sent Event with multiple data lines if necessary" do
49
+ expect(server_sent_event("hello\nworld\n!")).
50
+ to match(/\Adata: ?hello\ndata: ?world\ndata: ?!\n\n\z/)
51
+ end
52
+
53
+ end
@@ -0,0 +1,7 @@
1
+ RSpec.configure do |config|
2
+ config.mock_with :rspec
3
+ config.expect_with :rspec
4
+ config.expect_with :rspec do |c|
5
+ c.syntax = :expect
6
+ end
7
+ end
@@ -0,0 +1,204 @@
1
+ require 'spec_helper'
2
+ require 'socket'
3
+ require 'net/http'
4
+ require 'strscan'
5
+
6
+ shared_context "system" do
7
+ # Classes are not accessible from before, after hooks
8
+ # methods are
9
+ def run_server(*args)
10
+ Server.new(*args)
11
+ end
12
+
13
+ Event = Struct.new :data, :type, :id
14
+
15
+ class EventReader
16
+ def self.open(*args, &block)
17
+ reader = new(*args)
18
+ if block_given?
19
+ yield_and_close(reader, &block)
20
+ else
21
+ reader
22
+ end
23
+ end
24
+
25
+ class << self
26
+ private :new
27
+
28
+ private
29
+ def yield_and_close(reader)
30
+ yield reader
31
+ ensure
32
+ reader.close
33
+ end
34
+ end
35
+
36
+ def initialize(uri, last_event_id = nil)
37
+ @uri = URI(uri)
38
+ @last_event_id = last_event_id
39
+ @queue = Queue.new
40
+ @thread = Thread.new { connect }
41
+ @thread.abort_on_exception = true
42
+ event = @queue.pop
43
+ fail ':connected expected' unless event == :connected
44
+ end
45
+
46
+ attr_reader :response
47
+
48
+ CloseConnection = Class.new StandardError
49
+ def close
50
+ @closed_at = Time.now.to_f
51
+ @thread.raise CloseConnection
52
+ end
53
+
54
+ def connected?
55
+ return Net::HTTPOK === response && !@closed
56
+ end
57
+
58
+ # #each is blocking while the connection persists
59
+ # call #close in the given block to make #each return
60
+ def each
61
+ return enum_for(:each) unless block_given?
62
+ return unless connected?
63
+ while (event = @queue.pop) != :over
64
+ yield event
65
+ end
66
+ end
67
+
68
+ def full_stream
69
+ raise "No stream: response was #@response" unless @scanner
70
+ @scanner.full_stream
71
+ end
72
+
73
+ private
74
+
75
+ def connect
76
+ Net::HTTP.start(@uri.host, @uri.port) do |http|
77
+ response_received = false
78
+ headers = { 'Accept' => 'text/event-stream' }
79
+ headers['Last-Event-Id'] = @last_event_id.to_s if @last_event_id
80
+ http.request_get @uri, headers do |response|
81
+ # Fix a bug? in Net::HTTP where if the connection times out,
82
+ # the block runs again
83
+ return if response_received
84
+ response_received = true
85
+ self.response = response
86
+ read_events if connected?
87
+ end
88
+ end
89
+ rescue CloseConnection
90
+ ensure
91
+ @closed = true
92
+ @queue << :over
93
+ end
94
+
95
+ def response=(response)
96
+ @response = response
97
+ @scanner = EventScanner.new { |event| @queue << event }
98
+ @queue << :connected
99
+ end
100
+
101
+ def read_events
102
+ @response.read_body do |segment|
103
+ @scanner << segment
104
+ break if @closed
105
+ end
106
+ end
107
+ end
108
+
109
+ class EventScanner
110
+ def initialize(&block)
111
+ @body = StringScanner.new ""
112
+ @event = Event.new ""
113
+ @block = block
114
+ end
115
+
116
+ def <<(segment)
117
+ @body << segment
118
+ while lines = @body.scan_until(/\n\n/)
119
+ lines.split("\n").each do |line|
120
+ field, value = line.split(/: ?/, 2)
121
+ next if field.empty?
122
+ case field
123
+ when "id"
124
+ @event.id = value
125
+ when "event"
126
+ @event.type = value
127
+ when "data"
128
+ @event.data << value if value
129
+ @event.data << "\n"
130
+ end
131
+ end
132
+ @event.data.chomp!
133
+ @block.call @event
134
+ @event = Event.new ""
135
+ end
136
+ end
137
+
138
+ def full_stream
139
+ @body.string.dup
140
+ end
141
+ end
142
+
143
+ class Server
144
+ def initialize(command, port, pidfile: nil)
145
+ @command = command
146
+ @port = port
147
+ @pidfile = pidfile
148
+ check_tcp
149
+ start
150
+ end
151
+
152
+ def start
153
+ @pid = Process.spawn("#@command", %i(in out err) => :close)
154
+ if @pidfile
155
+ wait
156
+ @pid = nil
157
+ sleep(0.1) until File.exist?(@pidfile)
158
+ @pid = File.read(@pidfile).to_i if File.exist?(@pidfile)
159
+ end
160
+ end
161
+
162
+ def wait
163
+ if @pidfile
164
+ sleep(0.1) while File.exists?(@pidfile)
165
+ else
166
+ Process.wait(@pid)
167
+ end
168
+ rescue Errno::ESRCH
169
+ end
170
+
171
+ def stop
172
+ return unless @pid
173
+ Process.kill("TERM", @pid)
174
+ wait
175
+ ensure
176
+ @pid = nil
177
+ end
178
+
179
+ def wait_tcp
180
+ Timeout.timeout(5) do
181
+ _wait_tcp
182
+ end
183
+ rescue Timeout::Error
184
+ fail "Could not connect to server at localhost:#@port"
185
+ end
186
+
187
+ def _wait_tcp
188
+ connection = TCPSocket.new 'localhost', @port
189
+ self
190
+ rescue
191
+ retry
192
+ ensure
193
+ connection.close if connection
194
+ end
195
+
196
+ def check_tcp
197
+ connection = TCPSocket.new 'localhost', @port
198
+ fail "port #@port already used"
199
+ rescue Errno::ECONNREFUSED
200
+ ensure
201
+ connection.close if connection
202
+ end
203
+ end
204
+ end
metadata ADDED
@@ -0,0 +1,214 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redisse
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - "Étienne Barrié"
8
+ - Julien Blanchard
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-06-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.3'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.3'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: yard-tomdoc
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rack-test
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: dotenv
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: goliath
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: em-hiredis
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: redis
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :runtime
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ description:
141
+ email:
142
+ - etienne.barrie@gmail.com
143
+ - julien@sideburns.eu
144
+ executables:
145
+ - redisse
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - ".gitignore"
150
+ - ".rspec"
151
+ - ".ruby-version"
152
+ - ".travis.yml"
153
+ - Gemfile
154
+ - LICENSE.txt
155
+ - README.md
156
+ - Rakefile
157
+ - bin/redisse
158
+ - example/.env
159
+ - example/README.md
160
+ - example/bin/publish
161
+ - example/bin/redis
162
+ - example/config.ru
163
+ - example/nginx.conf
164
+ - example/public/index.html
165
+ - example/spec/app_spec.rb
166
+ - lib/redisse.rb
167
+ - lib/redisse/configuration.rb
168
+ - lib/redisse/publisher.rb
169
+ - lib/redisse/redirect_endpoint.rb
170
+ - lib/redisse/server.rb
171
+ - lib/redisse/server/redis.rb
172
+ - lib/redisse/server/responses.rb
173
+ - lib/redisse/server/stats.rb
174
+ - lib/redisse/server_sent_events.rb
175
+ - lib/redisse/version.rb
176
+ - redisse.gemspec
177
+ - spec/example_spec.rb
178
+ - spec/publisher_spec.rb
179
+ - spec/redirect_endpoint_spec.rb
180
+ - spec/server_sent_events_spec.rb
181
+ - spec/spec_helper.rb
182
+ - spec/spec_system_helper.rb
183
+ homepage: https://github.com/tigerlily/redisse
184
+ licenses:
185
+ - MIT
186
+ metadata: {}
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubyforge_project:
203
+ rubygems_version: 2.2.2
204
+ signing_key:
205
+ specification_version: 4
206
+ summary: Server-Sent Events via Redis
207
+ test_files:
208
+ - spec/example_spec.rb
209
+ - spec/publisher_spec.rb
210
+ - spec/redirect_endpoint_spec.rb
211
+ - spec/server_sent_events_spec.rb
212
+ - spec/spec_helper.rb
213
+ - spec/spec_system_helper.rb
214
+ has_rdoc: