angelo 0.1.14 → 0.1.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +4 -4
- data/README.md +101 -1
- data/lib/angelo/base.rb +58 -5
- data/lib/angelo/minitest/helpers.rb +13 -5
- data/lib/angelo/mustermann.rb +6 -1
- data/lib/angelo/responder/eventsource.rb +48 -0
- data/lib/angelo/responder/websocket.rb +39 -44
- data/lib/angelo/responder.rb +22 -14
- data/lib/angelo/server.rb +4 -1
- data/lib/angelo/stash.rb +70 -27
- data/lib/angelo/version.rb +1 -1
- data/lib/angelo.rb +5 -0
- data/test/angelo/eventsource_spec.rb +69 -0
- data/test/angelo/websocket_spec.rb +38 -3
- data/test/angelo_spec.rb +35 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3fe7e1fbbed26101e7b8204067db4e0cf8d8b24
|
4
|
+
data.tar.gz: 0d80c3741af219d78fdd2273717588ad286afa4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34fea99d0f4f8c0b4e10eaf2d1903df121d1ba541fb354e87f9b61caacfbcc4741894f6ec7bd8b4ccf1b7aab27770bf19d830b8a588afed113363c44d74fe26d
|
7
|
+
data.tar.gz: 52ee6e7b8111bdcf75c77ee828b061d831c921d27f50181d8f6d6f2b69abf1164498f23a0d01a846f920375214400330ce8ab880242cf3c50786e3ea780557b2
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
changelog
|
2
2
|
=========
|
3
3
|
|
4
|
+
### 0.1.15 24 jul 2014
|
5
|
+
|
6
|
+
* WebsocketResponder -> Responder::Websocket
|
7
|
+
* add Responder::Eventsource
|
8
|
+
* split Stash into module with Websocket and SSE classes
|
9
|
+
* add SSE eventsource route builder and helper
|
10
|
+
* add #event and #message to Stash::SSE
|
11
|
+
* add .sse_event and .sse_message to Base
|
12
|
+
|
4
13
|
### 0.1.14 3 jul 2014
|
5
14
|
|
6
15
|
* add send_file with disposition support
|
data/Gemfile.lock
CHANGED
@@ -15,7 +15,7 @@ GEM
|
|
15
15
|
httpclient (2.4.0)
|
16
16
|
method_source (0.8.2)
|
17
17
|
mime-types (2.3)
|
18
|
-
minitest (5.
|
18
|
+
minitest (5.4.0)
|
19
19
|
mustermann (0.2.0)
|
20
20
|
nio4r (1.0.0)
|
21
21
|
nio4r (1.0.0-java)
|
@@ -37,13 +37,13 @@ GEM
|
|
37
37
|
http_parser.rb (>= 0.6.0)
|
38
38
|
websocket_parser (>= 0.1.6)
|
39
39
|
ruby-prof (0.15.1)
|
40
|
-
slop (3.
|
40
|
+
slop (3.6.0)
|
41
41
|
spoon (0.0.4)
|
42
42
|
ffi
|
43
43
|
tilt (2.0.1)
|
44
44
|
timers (1.1.0)
|
45
|
-
websocket-driver (0.3.
|
46
|
-
websocket-driver (0.3.
|
45
|
+
websocket-driver (0.3.4)
|
46
|
+
websocket-driver (0.3.4-java)
|
47
47
|
websocket_parser (0.1.6)
|
48
48
|
|
49
49
|
PLATFORMS
|
data/README.md
CHANGED
@@ -8,7 +8,8 @@ A [Sinatra](https://github.com/sinatra/sinatra)-esque DSL for [Reel](https://git
|
|
8
8
|
### tl;dr
|
9
9
|
|
10
10
|
* websocket support via `websocket('/path'){|s| ... }` route builder
|
11
|
-
*
|
11
|
+
* SSE support via `eventsource('/path'){|s| ... }` route builder
|
12
|
+
* contextual websocket/sse stashing via `websockets` and `sses` helpers
|
12
13
|
* `task` handling via `async` and `future` helpers
|
13
14
|
* no rack
|
14
15
|
* optional tilt/erb support
|
@@ -99,6 +100,105 @@ When a `POST /` with a 'foo' param is received, any value is messaged out to any
|
|
99
100
|
websockets. When a `POST /bar` with a 'bar' param is received, any value is messaged out to all
|
100
101
|
websockets that connected to '/bar'.
|
101
102
|
|
103
|
+
### SSE - Server-Sent Events
|
104
|
+
|
105
|
+
The `eventsource` route builder also accepts a path and a block, and passes the socket to the block,
|
106
|
+
just like the `websocket` builder. This socket is actually the raw `Celluloid::IO::TCPSocket` and is
|
107
|
+
"detached" from the regular handling. There are no "on-*" methods; the `write` method should suffice.
|
108
|
+
To make it easier to deal with creation of the properly formatted Strings to send, Angelo provides
|
109
|
+
a couple helpers.
|
110
|
+
|
111
|
+
##### `sse_event` helper
|
112
|
+
|
113
|
+
To create an "event" that a javascript EventListener on the client can respond to:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
eventsource '/sse' do |s|
|
117
|
+
event = sse_event :foo, some_key: 'blah', other_key: 'boo'
|
118
|
+
s.write event
|
119
|
+
s.close
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
In this case, the EventListener would have to be configured to listen for the `foo` event:
|
124
|
+
|
125
|
+
```javascript
|
126
|
+
var sse = new EventSource('/sse');
|
127
|
+
sse.addEventListener('foo', function(e){ console.log("got foo event!\n" + JSON.parse(e.data)); });
|
128
|
+
```
|
129
|
+
|
130
|
+
The `sse_event` helper accepts a normal `String` for the data, but will automatically convert a `Hash`
|
131
|
+
argument to a JSON object.
|
132
|
+
|
133
|
+
##### `sse_message` helper
|
134
|
+
|
135
|
+
The `sse_message` helper behaves exactly the same as `sse_event`, but does not take an event name:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
eventsource '/sse' do |s|
|
139
|
+
msg = sse_msg some_key: 'blah', other_key: 'boo'
|
140
|
+
s.write msg
|
141
|
+
s.close
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
The client javascript would need to be altered to use the `EventSource.onmessage` property as well:
|
146
|
+
|
147
|
+
```javascript
|
148
|
+
var sse = new EventSource('/sse');
|
149
|
+
sse.onmessage = function(e){ console.log("got message!\n" + JSON.parse(e.data)); };
|
150
|
+
```
|
151
|
+
|
152
|
+
##### `sses` helper
|
153
|
+
|
154
|
+
Angelo also includes a "stash" helper for SSE connections. One can `<<` a socket into `sses` from
|
155
|
+
inside an `eventsource` handler block. These can also be "later" be iterated over so one can do things
|
156
|
+
like emit a message on every SSE connection when the service receives a POST request.
|
157
|
+
|
158
|
+
The `sses` helper includes the same a context ability as the `websockets` helper. In addition, the
|
159
|
+
`sses` stash includes methods for easily sending events or messages to all stashed connections. **Note
|
160
|
+
that the `Stash::SSE#event` method only works on non-default contexts and uses the context name as
|
161
|
+
the event name.**
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
eventsource '/sse' do |s|
|
165
|
+
sses[:foo] << s
|
166
|
+
end
|
167
|
+
|
168
|
+
post '/sse_message' do
|
169
|
+
sses[:foo].message params[:data]
|
170
|
+
end
|
171
|
+
|
172
|
+
post '/sse_event' do
|
173
|
+
sses[:foo].event params[:data]
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
##### `eventsource` instance helper
|
178
|
+
|
179
|
+
Additionally, you can also start SSE handling *conditionally* from inside a GET block:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
get '/sse_maybe' do
|
183
|
+
if params[:sse]
|
184
|
+
eventsource do |c|
|
185
|
+
sses << c
|
186
|
+
c.write sse_message 'wooo fancy SSE for you!'
|
187
|
+
end
|
188
|
+
else
|
189
|
+
'boring regular old get response'
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
post '/sse_event' do
|
194
|
+
sses.each {|sse| sse.write sse_event(:foo, 'fancy sse event!')}
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
Handling this on the client may require conditionals for [browsers](http://caniuse.com/eventsource) that
|
199
|
+
do not support EventSource yet, since this will respond with a non-"text/event-stream" Content-Type if
|
200
|
+
'sse' is not present in the params.
|
201
|
+
|
102
202
|
### Tasks + Async / Future
|
103
203
|
|
104
204
|
Angelo is built on Reel and Celluloid::IO, giving your web application class the ability to define
|
data/lib/angelo/base.rb
CHANGED
@@ -6,6 +6,7 @@ module Angelo
|
|
6
6
|
|
7
7
|
extend Forwardable
|
8
8
|
def_delegators :@responder, :content_type, :headers, :redirect, :request
|
9
|
+
def_delegators :@klass, :websockets, :sses, :sse_event, :sse_message
|
9
10
|
|
10
11
|
@@addr = DEFAULT_ADDR
|
11
12
|
@@port = DEFAULT_PORT
|
@@ -13,6 +14,8 @@ module Angelo
|
|
13
14
|
@@ping_time = DEFAULT_PING_TIME
|
14
15
|
@@log_level = DEFAULT_LOG_LEVEL
|
15
16
|
|
17
|
+
@@report_errors = false
|
18
|
+
|
16
19
|
if ARGV.any? and not Kernel.const_defined?('Minitest')
|
17
20
|
require 'optparse'
|
18
21
|
OptionParser.new { |op|
|
@@ -87,11 +90,15 @@ module Angelo
|
|
87
90
|
end
|
88
91
|
|
89
92
|
def websocket path, &block
|
90
|
-
routes[:websocket][path] =
|
93
|
+
routes[:websocket][path] = Responder::Websocket.new &block
|
94
|
+
end
|
95
|
+
|
96
|
+
def eventsource path, &block
|
97
|
+
routes[:get][path] = Responder::Eventsource.new &block
|
91
98
|
end
|
92
99
|
|
93
100
|
def on_pong &block
|
94
|
-
|
101
|
+
Responder::Websocket.on_pong = block
|
95
102
|
end
|
96
103
|
|
97
104
|
def task name, &block
|
@@ -99,11 +106,17 @@ module Angelo
|
|
99
106
|
end
|
100
107
|
|
101
108
|
def websockets
|
102
|
-
@websockets ||= Stash.new server
|
109
|
+
@websockets ||= Stash::Websocket.new server
|
103
110
|
@websockets.reject! &:closed?
|
104
111
|
@websockets
|
105
112
|
end
|
106
113
|
|
114
|
+
def sses
|
115
|
+
@sses ||= Stash::SSE.new server
|
116
|
+
@sses.reject! &:closed?
|
117
|
+
@sses
|
118
|
+
end
|
119
|
+
|
107
120
|
def content_type type
|
108
121
|
Responder.content_type type
|
109
122
|
end
|
@@ -126,6 +139,21 @@ module Angelo
|
|
126
139
|
end
|
127
140
|
end
|
128
141
|
|
142
|
+
def sse_event event_name, data
|
143
|
+
data = data.to_json if Hash === data
|
144
|
+
SSE_EVENT_TEMPLATE % [event_name.to_s, data]
|
145
|
+
end
|
146
|
+
|
147
|
+
def sse_message data
|
148
|
+
data = data.to_json if Hash === data
|
149
|
+
SSE_DATA_TEMPLATE % data
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
def initialize responder
|
155
|
+
@responder = responder
|
156
|
+
@klass = self.class
|
129
157
|
end
|
130
158
|
|
131
159
|
def async meth, *args
|
@@ -144,8 +172,6 @@ module Angelo
|
|
144
172
|
end
|
145
173
|
end
|
146
174
|
|
147
|
-
def websockets; self.class.websockets; end
|
148
|
-
|
149
175
|
def request_headers
|
150
176
|
@request_headers ||= Hash.new do |hash, key|
|
151
177
|
if Symbol === key
|
@@ -175,6 +201,19 @@ module Angelo
|
|
175
201
|
end
|
176
202
|
end
|
177
203
|
|
204
|
+
task :handle_event_source do |socket, block|
|
205
|
+
begin
|
206
|
+
block[socket]
|
207
|
+
rescue Reel::SocketError, IOError, SystemCallError => e
|
208
|
+
# probably closed on client
|
209
|
+
warn e.message if @@report_errors
|
210
|
+
socket.close unless socket.closed?
|
211
|
+
rescue => e
|
212
|
+
error e.inspect
|
213
|
+
socket.close unless socket.closed?
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
178
217
|
def halt status = 400, body = ''
|
179
218
|
throw :halt, HALT_STRUCT.new(status, body)
|
180
219
|
end
|
@@ -201,6 +240,20 @@ module Angelo
|
|
201
240
|
halt 200, File.read(lp)
|
202
241
|
end
|
203
242
|
|
243
|
+
def eventsource &block
|
244
|
+
headers SSE_HEADER
|
245
|
+
async :handle_event_source, responder.connection.detach.socket, block
|
246
|
+
halt 200, :sse
|
247
|
+
end
|
248
|
+
|
249
|
+
def report_errors?
|
250
|
+
@@report_errors
|
251
|
+
end
|
252
|
+
|
253
|
+
def sleep time
|
254
|
+
Celluloid.sleep time
|
255
|
+
end
|
256
|
+
|
204
257
|
end
|
205
258
|
|
206
259
|
end
|
@@ -31,14 +31,18 @@ module Angelo
|
|
31
31
|
@hc ||= HTTPClient.new
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
34
|
+
def url path = nil
|
35
35
|
url = HTTP_URL % [DEFAULT_ADDR, DEFAULT_PORT]
|
36
|
-
|
36
|
+
url += path if path
|
37
|
+
url
|
38
|
+
end
|
39
|
+
|
40
|
+
def hc_req method, path, params = {}, headers = {}
|
41
|
+
@last_response = hc.__send__ method, url(path), params, headers
|
37
42
|
end
|
38
43
|
private :hc_req
|
39
44
|
|
40
45
|
def http_req method, path, params = {}, headers = {}
|
41
|
-
url = HTTP_URL % [DEFAULT_ADDR, DEFAULT_PORT] + path
|
42
46
|
params = case params
|
43
47
|
when String; {body: params}
|
44
48
|
when Hash
|
@@ -51,9 +55,9 @@ module Angelo
|
|
51
55
|
end
|
52
56
|
@last_response = case
|
53
57
|
when !headers.empty?
|
54
|
-
::HTTP.with(headers).__send__ method, url, params
|
58
|
+
::HTTP.with(headers).__send__ method, url(path), params
|
55
59
|
else
|
56
|
-
::HTTP.__send__ method, url, params
|
60
|
+
::HTTP.__send__ method, url(path), params
|
57
61
|
end
|
58
62
|
end
|
59
63
|
private :http_req
|
@@ -65,6 +69,10 @@ module Angelo
|
|
65
69
|
end
|
66
70
|
end
|
67
71
|
|
72
|
+
def get_sse path, params = {}, headers = {}, &block
|
73
|
+
@last_response = hc.get url(path), params, headers, &block
|
74
|
+
end
|
75
|
+
|
68
76
|
def websocket_helper path, params = {}
|
69
77
|
params = params.keys.reduce([]) {|a,k|
|
70
78
|
a << CGI.escape(k) + '=' + CGI.escape(params[k])
|
data/lib/angelo/mustermann.rb
CHANGED
@@ -12,7 +12,7 @@ module Angelo
|
|
12
12
|
def_delegator :@responder, :mustermann
|
13
13
|
end
|
14
14
|
|
15
|
-
[Responder,
|
15
|
+
[Responder, Responder::Websocket, Responder::Eventsource].each do |res|
|
16
16
|
res.class_eval do
|
17
17
|
attr_accessor :mustermann
|
18
18
|
end
|
@@ -33,6 +33,11 @@ module Angelo
|
|
33
33
|
super path, &block
|
34
34
|
end
|
35
35
|
|
36
|
+
def eventsource path, &block
|
37
|
+
path = ::Mustermann.new path
|
38
|
+
super path, &block
|
39
|
+
end
|
40
|
+
|
36
41
|
def routes
|
37
42
|
@routes ||= {}
|
38
43
|
ROUTABLE.each do |m|
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Angelo
|
2
|
+
class Responder
|
3
|
+
class Eventsource < Responder
|
4
|
+
|
5
|
+
def request= request
|
6
|
+
@params = nil
|
7
|
+
@request = request
|
8
|
+
handle_request
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_request
|
12
|
+
begin
|
13
|
+
if @response_handler
|
14
|
+
@base.before if @base.respond_to? :before
|
15
|
+
@body = catch(:halt) { @base.eventsource &@response_handler.bind(@base) }
|
16
|
+
if HALT_STRUCT === @body
|
17
|
+
raise RequestError.new 'unknown sse error' unless @body.body == :sse
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO any real reason not to run afters with SSE?
|
21
|
+
# @base.after if @base.respond_to? :after
|
22
|
+
|
23
|
+
respond
|
24
|
+
else
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
rescue IOError => ioe
|
28
|
+
warn "#{ioe.class} - #{ioe.message}"
|
29
|
+
close_websocket
|
30
|
+
rescue => e
|
31
|
+
error e.message
|
32
|
+
::STDERR.puts e.backtrace
|
33
|
+
begin
|
34
|
+
@connection.close
|
35
|
+
rescue Reel::StateError => rcse
|
36
|
+
close_websocket
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond
|
42
|
+
Angelo.log @connection, @request, nil, :ok
|
43
|
+
@request.respond 200, headers, nil
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,60 +1,55 @@
|
|
1
1
|
module Angelo
|
2
|
+
class Responder
|
3
|
+
class Websocket < Responder
|
2
4
|
|
3
|
-
|
5
|
+
class << self
|
4
6
|
|
5
|
-
|
7
|
+
attr_writer :on_pong
|
6
8
|
|
7
|
-
|
9
|
+
def on_pong
|
10
|
+
@on_pong ||= ->(e){}
|
11
|
+
end
|
8
12
|
|
9
|
-
def on_pong
|
10
|
-
@on_pong ||= ->(e){}
|
11
13
|
end
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
def request= request
|
21
|
-
@params = nil
|
22
|
-
@request = request
|
23
|
-
@websocket = @request.websocket
|
24
|
-
handle_request
|
25
|
-
end
|
15
|
+
def request= request
|
16
|
+
@params = nil
|
17
|
+
@request = request
|
18
|
+
@websocket = @request.websocket
|
19
|
+
handle_request
|
20
|
+
end
|
26
21
|
|
27
|
-
|
28
|
-
begin
|
29
|
-
if @response_handler
|
30
|
-
Angelo.log @connection, @request, @websocket, :switching_protocols
|
31
|
-
@bound_response_handler ||= @response_handler.bind @base
|
32
|
-
@websocket.on_pong &WebsocketResponder.on_pong
|
33
|
-
@base.before if @base.respond_to? :before
|
34
|
-
@bound_response_handler[@websocket]
|
35
|
-
@base.after if @base.respond_to? :after
|
36
|
-
else
|
37
|
-
raise NotImplementedError
|
38
|
-
end
|
39
|
-
rescue IOError => ioe
|
40
|
-
warn "#{ioe.class} - #{ioe.message}"
|
41
|
-
close_websocket
|
42
|
-
rescue => e
|
43
|
-
error e.message
|
44
|
-
::STDERR.puts e.backtrace
|
22
|
+
def handle_request
|
45
23
|
begin
|
46
|
-
@
|
47
|
-
|
24
|
+
if @response_handler
|
25
|
+
Angelo.log @connection, @request, @websocket, :switching_protocols
|
26
|
+
@bound_response_handler ||= @response_handler.bind @base
|
27
|
+
@websocket.on_pong &Responder::Websocket.on_pong
|
28
|
+
@base.before if @base.respond_to? :before
|
29
|
+
@bound_response_handler[@websocket]
|
30
|
+
@base.after if @base.respond_to? :after
|
31
|
+
else
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
rescue IOError => ioe
|
35
|
+
warn "#{ioe.class} - #{ioe.message}"
|
48
36
|
close_websocket
|
37
|
+
rescue => e
|
38
|
+
error e.message
|
39
|
+
::STDERR.puts e.backtrace
|
40
|
+
begin
|
41
|
+
@connection.close
|
42
|
+
rescue Reel::StateError => rcse
|
43
|
+
close_websocket
|
44
|
+
end
|
49
45
|
end
|
50
46
|
end
|
51
|
-
end
|
52
47
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
48
|
+
def close_websocket
|
49
|
+
@websocket.close
|
50
|
+
@base.websockets.remove_socket @websocket
|
51
|
+
end
|
57
52
|
|
53
|
+
end
|
58
54
|
end
|
59
|
-
|
60
55
|
end
|
data/lib/angelo/responder.rb
CHANGED
@@ -32,18 +32,14 @@ module Angelo
|
|
32
32
|
|
33
33
|
end
|
34
34
|
|
35
|
-
|
35
|
+
attr_accessor :connection
|
36
36
|
attr_reader :request
|
37
|
+
attr_writer :base
|
37
38
|
|
38
39
|
def initialize &block
|
39
40
|
@response_handler = Base.compile! :request_handler, &block
|
40
41
|
end
|
41
42
|
|
42
|
-
def base= base
|
43
|
-
@base = base
|
44
|
-
@base.responder = self
|
45
|
-
end
|
46
|
-
|
47
43
|
def request= request
|
48
44
|
@params = nil
|
49
45
|
@redirect = nil
|
@@ -56,22 +52,32 @@ module Angelo
|
|
56
52
|
if @response_handler
|
57
53
|
@base.before if @base.respond_to? :before
|
58
54
|
@body = catch(:halt) { @response_handler.bind(@base).call || EMPTY_STRING }
|
59
|
-
|
55
|
+
|
56
|
+
# TODO any real reason not to run afters with SSE?
|
57
|
+
case @body
|
58
|
+
when HALT_STRUCT
|
59
|
+
if @body.body != :sse and @base.respond_to? :after
|
60
|
+
@base.after
|
61
|
+
end
|
62
|
+
else
|
63
|
+
@base.after if @base.respond_to? :after
|
64
|
+
end
|
65
|
+
|
60
66
|
respond
|
61
67
|
else
|
62
68
|
raise NotImplementedError
|
63
69
|
end
|
64
70
|
rescue JSON::ParserError => jpe
|
65
|
-
handle_error jpe, :bad_request
|
71
|
+
handle_error jpe, :bad_request
|
66
72
|
rescue FormEncodingError => fee
|
67
|
-
handle_error fee, :bad_request
|
73
|
+
handle_error fee, :bad_request
|
68
74
|
rescue RequestError => re
|
69
|
-
handle_error re, re.type
|
75
|
+
handle_error re, re.type
|
70
76
|
rescue => e
|
71
77
|
handle_error e
|
72
78
|
end
|
73
79
|
|
74
|
-
def handle_error _error, type = :internal_server_error, report =
|
80
|
+
def handle_error _error, type = :internal_server_error, report = @base.report_errors?
|
75
81
|
err_msg = error_message _error
|
76
82
|
Angelo.log @connection, @request, nil, type, err_msg.size
|
77
83
|
@connection.respond type, headers, err_msg
|
@@ -130,6 +136,7 @@ module Angelo
|
|
130
136
|
when HALT_STRUCT
|
131
137
|
status = @body.status
|
132
138
|
@body = @body.body
|
139
|
+
@body = nil if @body == :sse
|
133
140
|
if Hash === @body
|
134
141
|
@body = {error: @body} if status != :ok or status < 200 && status >= 300
|
135
142
|
@body = @body.to_json if respond_with? :json
|
@@ -148,11 +155,12 @@ module Angelo
|
|
148
155
|
|
149
156
|
status ||= @redirect.nil? ? :ok : :moved_permanently
|
150
157
|
headers LOCATION_HEADER_KEY => @redirect if @redirect
|
158
|
+
size = @body.nil? ? 0 : @body.size
|
151
159
|
|
152
|
-
Angelo.log @connection, @request, nil, status,
|
153
|
-
@
|
160
|
+
Angelo.log @connection, @request, nil, status, size
|
161
|
+
@request.respond status, headers, @body
|
154
162
|
rescue => e
|
155
|
-
handle_error e, :internal_server_error
|
163
|
+
handle_error e, :internal_server_error
|
156
164
|
end
|
157
165
|
|
158
166
|
def redirect url
|
data/lib/angelo/server.rb
CHANGED
@@ -47,7 +47,7 @@ module Angelo
|
|
47
47
|
rs = @base.routes[meth][request.path]
|
48
48
|
if rs
|
49
49
|
responder = rs.dup
|
50
|
-
responder.base = @base.new
|
50
|
+
responder.base = @base.new responder
|
51
51
|
responder.connection = connection
|
52
52
|
responder.request = request
|
53
53
|
else
|
@@ -92,6 +92,9 @@ module Angelo
|
|
92
92
|
OpenSSL::Digest::SHA.hexdigest fs.ino.to_s + fs.size.to_s + fs.mtime.to_s
|
93
93
|
end
|
94
94
|
|
95
|
+
def sse_event *a; Base.sse_event *a; end
|
96
|
+
def sse_message *a; Base.sse_message *a; end
|
97
|
+
|
95
98
|
end
|
96
99
|
|
97
100
|
end
|
data/lib/angelo/stash.rb
CHANGED
@@ -2,62 +2,75 @@ module Angelo
|
|
2
2
|
|
3
3
|
# utility class for stashing connected websockets in arbitrary contexts
|
4
4
|
#
|
5
|
-
|
5
|
+
module Stash
|
6
6
|
include Celluloid::Logger
|
7
7
|
|
8
|
-
|
9
|
-
#
|
10
|
-
@@peeraddrs = {}
|
8
|
+
module ClassMethods
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
# the underlying arrays of websockets, by context
|
11
|
+
#
|
12
|
+
def stashes
|
13
|
+
@stashes ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# hold the peeraddr info for use after the socket is closed (logging)
|
17
|
+
#
|
18
|
+
def peeraddrs
|
19
|
+
@peeraddrs ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def stashes; self.class.stashes; end
|
25
|
+
def peeraddrs; self.class.peeraddrs; end
|
15
26
|
|
16
27
|
# create a new instance with a context, creating the array if needed
|
17
28
|
#
|
18
29
|
def initialize server, context = :default
|
19
30
|
raise ArgumentError.new "symbol required" unless Symbol === context
|
20
31
|
@context, @server = context, server
|
21
|
-
|
32
|
+
stashes[@context] ||= []
|
22
33
|
end
|
23
34
|
|
24
35
|
# add a websocket to this context's stash, save peeraddr info, start
|
25
36
|
# server handle_websocket task to read from the socket and fire events
|
26
37
|
# as needed
|
27
38
|
#
|
28
|
-
def <<
|
29
|
-
|
30
|
-
|
31
|
-
|
39
|
+
def << s
|
40
|
+
peeraddrs[s] = s.peeraddr
|
41
|
+
yield if block_given?
|
42
|
+
stashes[@context] << s
|
32
43
|
end
|
33
44
|
|
34
45
|
# access the underlying array of this context
|
35
46
|
#
|
36
47
|
def stash
|
37
|
-
|
48
|
+
stashes[@context]
|
38
49
|
end
|
39
50
|
|
40
51
|
# iterate on each connected websocket in this context, handling errors
|
41
52
|
# as needed
|
42
53
|
#
|
43
54
|
def each &block
|
44
|
-
stash.each do |
|
55
|
+
stash.each do |s|
|
45
56
|
begin
|
46
|
-
yield
|
57
|
+
yield s
|
47
58
|
rescue Reel::SocketError, IOError, SystemCallError => e
|
48
59
|
debug e.message
|
49
|
-
remove_socket
|
60
|
+
remove_socket s
|
50
61
|
end
|
51
62
|
end
|
63
|
+
nil
|
52
64
|
end
|
53
65
|
|
54
|
-
# remove a
|
66
|
+
# remove a socket from the stash, warn user, drop peeraddr info
|
55
67
|
#
|
56
|
-
def remove_socket
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
68
|
+
def remove_socket s
|
69
|
+
s.close unless s.closed?
|
70
|
+
if stash.include? s
|
71
|
+
warn "removing socket from context ':#{@context}' (#{peeraddrs[s][2]})"
|
72
|
+
stash.delete s
|
73
|
+
peeraddrs.delete s
|
61
74
|
end
|
62
75
|
end
|
63
76
|
|
@@ -71,12 +84,12 @@ module Angelo
|
|
71
84
|
# ping_websockets task
|
72
85
|
#
|
73
86
|
def all_each
|
74
|
-
|
87
|
+
stashes.values.flatten.each do |s|
|
75
88
|
begin
|
76
|
-
yield
|
89
|
+
yield s
|
77
90
|
rescue Reel::SocketError, IOError, SystemCallError => e
|
78
91
|
debug e.message
|
79
|
-
remove_socket
|
92
|
+
remove_socket s
|
80
93
|
end
|
81
94
|
end
|
82
95
|
end
|
@@ -89,8 +102,8 @@ module Angelo
|
|
89
102
|
|
90
103
|
# access the peeraddr info for a given websocket
|
91
104
|
#
|
92
|
-
def peeraddr
|
93
|
-
|
105
|
+
def peeraddr s
|
106
|
+
peeraddrs[s]
|
94
107
|
end
|
95
108
|
|
96
109
|
# return the number of websockets in this context (some are potentially
|
@@ -100,5 +113,35 @@ module Angelo
|
|
100
113
|
stash.length
|
101
114
|
end
|
102
115
|
|
116
|
+
class Websocket
|
117
|
+
extend Stash::ClassMethods
|
118
|
+
include Stash
|
119
|
+
|
120
|
+
def << ws
|
121
|
+
super do
|
122
|
+
@server.async.handle_websocket ws
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
class SSE
|
129
|
+
extend Stash::ClassMethods
|
130
|
+
include Stash
|
131
|
+
|
132
|
+
def event data
|
133
|
+
raise ArgumentError.new 'use #message method for "messages"' if @context == :default
|
134
|
+
each {|s| s.write Angelo::Base.sse_event(@context, data)}
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def message data
|
139
|
+
each {|s| s.write Angelo::Base.sse_message(data)}
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
103
145
|
end
|
146
|
+
|
104
147
|
end
|
data/lib/angelo/version.rb
CHANGED
data/lib/angelo.rb
CHANGED
@@ -25,6 +25,7 @@ module Angelo
|
|
25
25
|
ETAG_HEADER_KEY = 'ETag'
|
26
26
|
IF_NONE_MATCH_HEADER_KEY = 'If-None-Match'
|
27
27
|
LOCATION_HEADER_KEY = 'Location'
|
28
|
+
SSE_HEADER = { CONTENT_TYPE_HEADER_KEY => 'text/event-stream' }
|
28
29
|
|
29
30
|
HTML_TYPE = 'text/html'
|
30
31
|
JSON_TYPE = 'application/json'
|
@@ -57,6 +58,9 @@ module Angelo
|
|
57
58
|
|
58
59
|
HALT_STRUCT = Struct.new :status, :body
|
59
60
|
|
61
|
+
SSE_DATA_TEMPLATE = "data: %s\n\n"
|
62
|
+
SSE_EVENT_TEMPLATE = "event: %s\ndata: %s\n\n"
|
63
|
+
|
60
64
|
class << self
|
61
65
|
|
62
66
|
attr_writer :response_log_level
|
@@ -122,6 +126,7 @@ require 'angelo/server'
|
|
122
126
|
require 'angelo/base'
|
123
127
|
require 'angelo/stash'
|
124
128
|
require 'angelo/responder'
|
129
|
+
require 'angelo/responder/eventsource'
|
125
130
|
require 'angelo/responder/websocket'
|
126
131
|
|
127
132
|
# trap "INT" do
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe Angelo::Responder::Eventsource do
|
4
|
+
|
5
|
+
describe 'route builder' do
|
6
|
+
|
7
|
+
define_app do
|
8
|
+
|
9
|
+
eventsource '/msg' do |c|
|
10
|
+
c.write sse_message 'hi'
|
11
|
+
c.close
|
12
|
+
end
|
13
|
+
|
14
|
+
eventsource '/event' do |c|
|
15
|
+
c.write sse_event :sse, 'bye'
|
16
|
+
c.close
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'sends messages' do
|
22
|
+
get_sse '/msg' do |msg|
|
23
|
+
msg.must_equal "data: hi\n\n"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'sends events' do
|
28
|
+
get_sse '/event' do |msg|
|
29
|
+
msg.must_equal "event: sse\ndata: bye\n\n"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'eventsource helper' do
|
38
|
+
|
39
|
+
define_app do
|
40
|
+
|
41
|
+
get '/msg' do
|
42
|
+
eventsource do |c|
|
43
|
+
c.write sse_message 'hi'
|
44
|
+
c.close
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
get '/event' do
|
49
|
+
eventsource do |c|
|
50
|
+
c.write sse_event :sse, 'bye'
|
51
|
+
c.close
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'sends messages' do
|
58
|
+
get_sse '/msg' do |msg|
|
59
|
+
msg.must_equal "data: hi\n\n"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'sends events' do
|
64
|
+
get_sse '/event' do |msg|
|
65
|
+
msg.must_equal "event: sse\ndata: bye\n\n"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require_relative '../spec_helper'
|
2
2
|
|
3
|
-
describe Angelo::
|
3
|
+
describe Angelo::Responder::Websocket do
|
4
4
|
|
5
5
|
def websocket_wait_for path, latch, expectation, key = :swf, &block
|
6
6
|
Reactor.testers[key] = Array.new CONCURRENCY do
|
@@ -225,8 +225,14 @@ describe Angelo::WebsocketResponder do
|
|
225
225
|
end
|
226
226
|
|
227
227
|
describe 'helper contexts' do
|
228
|
-
|
229
|
-
|
228
|
+
|
229
|
+
def obj
|
230
|
+
{ 'foo' => 'bar' }
|
231
|
+
end
|
232
|
+
|
233
|
+
def wait_for_block
|
234
|
+
->(e){ assert_equal obj, JSON.parse(e.data) }
|
235
|
+
end
|
230
236
|
|
231
237
|
define_app do
|
232
238
|
|
@@ -345,4 +351,33 @@ describe Angelo::WebsocketResponder do
|
|
345
351
|
|
346
352
|
end
|
347
353
|
|
354
|
+
describe 'params' do
|
355
|
+
|
356
|
+
def wait_for_block
|
357
|
+
->(e){ assert_equal({'bar' => 'foo', 'baz' => 'bat'}, JSON.parse(e.data)) }
|
358
|
+
end
|
359
|
+
|
360
|
+
define_app do
|
361
|
+
|
362
|
+
websocket '/' do |ws|
|
363
|
+
websockets[params[:bar].to_sym] << ws
|
364
|
+
end
|
365
|
+
|
366
|
+
post '/' do
|
367
|
+
pj = params.to_json
|
368
|
+
websockets[params[:bar].to_sym].each {|ws| ws.write pj}
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
|
373
|
+
it 'uses params correctly' do
|
374
|
+
latch = CountDownLatch.new CONCURRENCY
|
375
|
+
websocket_wait_for '/?bar=foo', latch, wait_for_block do
|
376
|
+
post '/', bar: 'foo', baz: 'bat'
|
377
|
+
latch.wait
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
|
348
383
|
end
|
data/test/angelo_spec.rb
CHANGED
@@ -31,6 +31,11 @@ describe Angelo::Base do
|
|
31
31
|
redirect '/'
|
32
32
|
end
|
33
33
|
|
34
|
+
get '/wait' do
|
35
|
+
sleep 3
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
34
39
|
end
|
35
40
|
|
36
41
|
it 'responds to http requests properly' do
|
@@ -56,6 +61,36 @@ describe Angelo::Base do
|
|
56
61
|
last_response.headers['Location'].must_equal '/'
|
57
62
|
end
|
58
63
|
|
64
|
+
it 'responds to requests concurrently' do
|
65
|
+
wait_end = nil
|
66
|
+
get_end = nil
|
67
|
+
latch = CountDownLatch.new 2
|
68
|
+
|
69
|
+
ActorPool.define_action :do_wait do
|
70
|
+
get '/wait'
|
71
|
+
wait_end = Time.now
|
72
|
+
latch.count_down
|
73
|
+
end
|
74
|
+
|
75
|
+
ActorPool.define_action :do_get do
|
76
|
+
sleep 1
|
77
|
+
get '/'
|
78
|
+
get_end = Time.now
|
79
|
+
latch.count_down
|
80
|
+
end
|
81
|
+
|
82
|
+
ActorPool.unstop!
|
83
|
+
$pool.async :do_wait
|
84
|
+
$pool.async :do_get
|
85
|
+
|
86
|
+
latch.wait
|
87
|
+
get_end.must_be :<, wait_end
|
88
|
+
|
89
|
+
ActorPool.stop!
|
90
|
+
ActorPool.remove_action :do_wait
|
91
|
+
ActorPool.remove_action :do_get
|
92
|
+
end
|
93
|
+
|
59
94
|
end
|
60
95
|
|
61
96
|
describe 'before filter' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: angelo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Nakamura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-07-
|
11
|
+
date: 2014-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: reel
|
@@ -60,6 +60,7 @@ files:
|
|
60
60
|
- lib/angelo/mustermann.rb
|
61
61
|
- lib/angelo/params_parser.rb
|
62
62
|
- lib/angelo/responder.rb
|
63
|
+
- lib/angelo/responder/eventsource.rb
|
63
64
|
- lib/angelo/responder/websocket.rb
|
64
65
|
- lib/angelo/server.rb
|
65
66
|
- lib/angelo/stash.rb
|
@@ -67,6 +68,7 @@ files:
|
|
67
68
|
- lib/angelo/version.rb
|
68
69
|
- test/angelo/erb_spec.rb
|
69
70
|
- test/angelo/error_spec.rb
|
71
|
+
- test/angelo/eventsource_spec.rb
|
70
72
|
- test/angelo/mustermann_spec.rb
|
71
73
|
- test/angelo/params_spec.rb
|
72
74
|
- test/angelo/static_spec.rb
|