angelo 0.1.14 → 0.1.15
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 +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
|